diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /vcl/source/control | |
parent | Initial commit. (diff) | |
download | libreoffice-upstream.tar.xz libreoffice-upstream.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vcl/source/control')
42 files changed, 39988 insertions, 0 deletions
diff --git a/vcl/source/control/ContextVBox.cxx b/vcl/source/control/ContextVBox.cxx new file mode 100644 index 000000000..94090d5cd --- /dev/null +++ b/vcl/source/control/ContextVBox.cxx @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <vcl/NotebookbarContextControl.hxx> +#include <ContextVBox.hxx> + +ContextVBox::ContextVBox( vcl::Window *pParent ) + : VclVBox( pParent ) +{ +} + +ContextVBox::~ContextVBox() +{ + disposeOnce(); +} + +void ContextVBox::SetContext( vcl::EnumContext::Context eContext ) +{ + for (int nChild = 0; nChild < GetChildCount(); ++nChild) + { + if ( GetChild( nChild )->GetType() == WindowType::CONTAINER ) + { + VclContainer* pChild = static_cast<VclContainer*>( GetChild( nChild ) ); + + if ( pChild->HasContext( eContext ) || pChild->HasContext( vcl::EnumContext::Context::Any ) ) + { + Size aSize( pChild->GetOptimalSize() ); + aSize.AdjustHeight(6 ); + pChild->Show(); + pChild->SetSizePixel( aSize ); + } + else + { + pChild->Hide(); + pChild->SetSizePixel( Size( 0, 0 ) ); + } + } + } + Size aSize( GetOptimalSize() ); + aSize.AdjustWidth(6 ); + SetSizePixel( aSize ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/DropdownBox.cxx b/vcl/source/control/DropdownBox.cxx new file mode 100644 index 000000000..6aaf2e553 --- /dev/null +++ b/vcl/source/control/DropdownBox.cxx @@ -0,0 +1,115 @@ +/* -*- 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 <vcl/toolkit/button.hxx> +#include <vcl/layout.hxx> +#include <DropdownBox.hxx> + +#define NOTEBOOK_HEADER_HEIGHT 30 + +/* + * DropdownBox - shows content or moves it to the popup + * which can be opened by clicking on a button + */ + +DropdownBox::DropdownBox(vcl::Window* pParent) + : VclHBox(pParent) + , m_bInFullView(true) +{ + m_pButton = VclPtr<PushButton>::Create(this, WB_FLATBUTTON); + m_pButton->SetClickHdl(LINK(this, DropdownBox, PBClickHdl)); + m_pButton->SetSymbol(SymbolType::MENU); + m_pButton->set_width_request(15); + m_pButton->SetQuickHelpText(GetQuickHelpText()); + m_pButton->Resize(); +} + +DropdownBox::~DropdownBox() { disposeOnce(); } + +void DropdownBox::dispose() +{ + m_pButton.disposeAndClear(); + if (m_pPopup) + m_pPopup.disposeAndClear(); + + VclHBox::dispose(); +} + +void DropdownBox::HideContent() +{ + if (m_bInFullView) + { + m_bInFullView = false; + + for (int i = 0; i < GetChildCount(); i++) + GetChild(i)->Hide(); + + m_pButton->Show(); + SetOutputSizePixel(Size(m_pButton->GetSizePixel().Width(), GetSizePixel().Height())); + } +} + +bool DropdownBox::IsHidden() { return !m_bInFullView; } + +void DropdownBox::ShowContent() +{ + if (!m_bInFullView) + { + m_bInFullView = true; + + for (int i = 0; i < GetChildCount(); i++) + GetChild(i)->Show(); + + m_pButton->Hide(); + } +} + +IMPL_LINK(DropdownBox, PBClickHdl, Button*, /*pButton*/, void) +{ + if (m_pPopup) + m_pPopup.disposeAndClear(); + + m_pPopup = VclPtr<NotebookbarPopup>::Create(this); + + for (int i = 0; i < GetChildCount(); i++) + { + if (GetChild(i) != m_pButton) + { + Window* pChild = GetChild(i); + pChild->Show(); + + pChild->SetParent(m_pPopup->getBox()); + // count is decreased because we moved child + i--; + } + } + + m_pPopup->hideSeparators(true); + + m_pPopup->getBox()->set_height_request(GetSizePixel().Height()); + + tools::Long x = GetPosPixel().getX(); + tools::Long y = GetPosPixel().getY() + NOTEBOOK_HEADER_HEIGHT + GetSizePixel().Height(); + tools::Rectangle aRect(x, y, x, y); + + m_pPopup->StartPopupMode(aRect, FloatWinPopupFlags::Down | FloatWinPopupFlags::GrabFocus + | FloatWinPopupFlags::AllMouseButtonClose); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/InterimItemWindow.cxx b/vcl/source/control/InterimItemWindow.cxx new file mode 100644 index 000000000..0017065d7 --- /dev/null +++ b/vcl/source/control/InterimItemWindow.cxx @@ -0,0 +1,195 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <vcl/InterimItemWindow.hxx> +#include <vcl/layout.hxx> +#include <salobj.hxx> +#include <window.h> + +InterimItemWindow::InterimItemWindow(vcl::Window* pParent, const OUString& rUIXMLDescription, + const OString& rID, bool bAllowCycleFocusOut, + sal_uInt64 nLOKWindowId) + : Control(pParent, WB_TABSTOP) + , m_pWidget(nullptr) // inheritors are expected to call InitControlBase + , m_aLayoutIdle("InterimItemWindow m_aLayoutIdle") +{ + m_aLayoutIdle.SetPriority(TaskPriority::RESIZE); + m_aLayoutIdle.SetInvokeHandler(LINK(this, InterimItemWindow, DoLayout)); + + m_xVclContentArea = VclPtr<VclVBox>::Create(this); + m_xVclContentArea->Show(); + m_xBuilder = Application::CreateInterimBuilder(m_xVclContentArea, rUIXMLDescription, + bAllowCycleFocusOut, nLOKWindowId); + m_xContainer = m_xBuilder->weld_container(rID); + + SetBackground(); + SetPaintTransparent(true); +} + +void InterimItemWindow::StateChanged(StateChangedType nStateChange) +{ + if (nStateChange == StateChangedType::Enable) + m_xContainer->set_sensitive(IsEnabled()); + Control::StateChanged(nStateChange); +} + +InterimItemWindow::~InterimItemWindow() { disposeOnce(); } + +void InterimItemWindow::dispose() +{ + m_pWidget = nullptr; + + m_xContainer.reset(); + m_xBuilder.reset(); + m_xVclContentArea.disposeAndClear(); + + m_aLayoutIdle.Stop(); + + Control::dispose(); +} + +void InterimItemWindow::StartIdleLayout() +{ + if (!m_xVclContentArea) + return; + if (m_aLayoutIdle.IsActive()) + return; + m_aLayoutIdle.Start(); +} + +void InterimItemWindow::queue_resize(StateChangedType eReason) +{ + Control::queue_resize(eReason); + StartIdleLayout(); +} + +void InterimItemWindow::Resize() { Layout(); } + +void InterimItemWindow::UnclipVisibleSysObj() +{ + if (!IsVisible()) + return; + vcl::Window* pChild = m_xVclContentArea->GetWindow(GetWindowType::FirstChild); + if (!pChild) + return; + WindowImpl* pWindowImpl = pChild->ImplGetWindowImpl(); + if (!pWindowImpl) + return; + if (!pWindowImpl->mpSysObj) + return; + pWindowImpl->mpSysObj->Show(true); + pWindowImpl->mpSysObj->ResetClipRegion(); + // flag that sysobj clip is dirty and needs to be recalculated on next use + pWindowImpl->mbInitWinClipRegion = true; +} + +IMPL_LINK_NOARG(InterimItemWindow, DoLayout, Timer*, void) { Layout(); } + +void InterimItemWindow::Layout() +{ + m_aLayoutIdle.Stop(); + vcl::Window* pChild = GetWindow(GetWindowType::FirstChild); + assert(pChild); + VclContainer::setLayoutAllocation(*pChild, Point(0, 0), GetSizePixel()); + Control::Resize(); +} + +Size InterimItemWindow::GetOptimalSize() const +{ + return VclContainer::getLayoutRequisition(*GetWindow(GetWindowType::FirstChild)); +} + +void InterimItemWindow::InvalidateChildSizeCache() +{ + // find the bottom vcl::Window of the hierarchy and queue_resize on that + // one will invalidate all the size caches upwards + vcl::Window* pChild = GetWindow(GetWindowType::FirstChild); + while (true) + { + vcl::Window* pSubChild = pChild->GetWindow(GetWindowType::FirstChild); + if (!pSubChild) + break; + pChild = pSubChild; + } + pChild->queue_resize(); +} + +bool InterimItemWindow::ControlHasFocus() const +{ + if (!m_pWidget) + return false; + return m_pWidget->has_focus(); +} + +void InterimItemWindow::InitControlBase(weld::Widget* pWidget) { m_pWidget = pWidget; } + +void InterimItemWindow::GetFocus() +{ + if (m_pWidget) + m_pWidget->grab_focus(); + + /* let toolbox know this item window has focus so it updates its mnHighItemId to point + to this toolitem in case tab means to move to another toolitem within + the toolbox + */ + vcl::Window* pToolBox = GetParent(); + NotifyEvent aNEvt(MouseNotifyEvent::GETFOCUS, this); + pToolBox->EventNotify(aNEvt); +} + +bool InterimItemWindow::ChildKeyInput(const KeyEvent& rKEvt) +{ + sal_uInt16 nCode = rKEvt.GetKeyCode().GetCode(); + if (nCode != KEY_TAB) + return false; + + /* if the native widget has focus, then no vcl window has focus. + + We want to grab focus to this vcl widget so that pressing tab will traverse + to the next vcl widget. + + But just using GrabFocus will, because no vcl widget has focus, trigger + bringing the toplevel to front with the expectation that a suitable widget + will be picked for focus when that happen, which is no use to us here. + + SetFakeFocus avoids the problem, allowing GrabFocus to do the expected thing + then sending the Tab to our parent will do the right traversal + */ + SetFakeFocus(true); + GrabFocus(); + + /* now give focus to our toolbox parent */ + vcl::Window* pToolBox = GetParent(); + pToolBox->GrabFocus(); + + /* let toolbox know this item window has focus so it updates its mnHighItemId to point + to this toolitem in case tab means to move to another toolitem within + the toolbox + */ + NotifyEvent aNEvt(MouseNotifyEvent::GETFOCUS, this); + pToolBox->EventNotify(aNEvt); + + /* send parent the tab */ + pToolBox->KeyInput(rKEvt); + + return true; +} + +void InterimItemWindow::Draw(OutputDevice* pDevice, const Point& rPos, + SystemTextColorFlags /*nFlags*/) +{ + m_xContainer->draw(*pDevice, rPos, GetSizePixel()); +} + +void InterimItemWindow::ImplPaintToDevice(::OutputDevice* pTargetOutDev, const Point& rPos) +{ + Draw(pTargetOutDev, rPos, SystemTextColorFlags::NONE); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/source/control/NotebookbarPopup.cxx b/vcl/source/control/NotebookbarPopup.cxx new file mode 100644 index 000000000..3cc21815a --- /dev/null +++ b/vcl/source/control/NotebookbarPopup.cxx @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <vcl/bitmapex.hxx> +#include <vcl/builder.hxx> +#include <vcl/layout.hxx> +#include <IPrioritable.hxx> +#include <NotebookbarPopup.hxx> + +NotebookbarPopup::NotebookbarPopup(const VclPtr<VclHBox>& pParent) + : FloatingWindow(pParent, "Popup", "sfx/ui/notebookbarpopup.ui") + , m_pParent(pParent) +{ + m_pUIBuilder->get(m_pBox, "box"); + m_pBox->SetSizePixel(Size(100, 75)); + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + const BitmapEx& aPersona = rStyleSettings.GetPersonaHeader(); + + if (!aPersona.IsEmpty()) + m_pBox->SetBackground(Wallpaper(aPersona)); + else + m_pBox->SetBackground(rStyleSettings.GetDialogColor()); +} + +NotebookbarPopup::~NotebookbarPopup() { disposeOnce(); } + +VclHBox* NotebookbarPopup::getBox() { return m_pBox.get(); } + +void NotebookbarPopup::PopupModeEnd() +{ + hideSeparators(false); + while (m_pBox->GetChildCount()) + { + vcl::IPrioritable* pChild = dynamic_cast<vcl::IPrioritable*>(GetChild(0)); + if (pChild) + pChild->HideContent(); + + vcl::Window* pWindow = m_pBox->GetChild(0); + pWindow->SetParent(m_pParent); + + // resize after all children of box are empty + if (m_pParent && !m_pBox->GetChildCount()) + m_pParent->Resize(); + } + + FloatingWindow::PopupModeEnd(); +} + +void NotebookbarPopup::hideSeparators(bool bHide) +{ + // separator on the beginning + vcl::Window* pWindow = m_pBox->GetChild(0); + while (pWindow && pWindow->GetType() == WindowType::CONTAINER) + { + pWindow = pWindow->GetChild(0); + } + if (pWindow && pWindow->GetType() == WindowType::FIXEDLINE) + { + if (bHide) + pWindow->Hide(); + else + pWindow->Show(); + } + + // separator on the end + pWindow = m_pBox->GetChild(m_pBox->GetChildCount() - 1); + while (pWindow && pWindow->GetType() == WindowType::CONTAINER) + { + pWindow = pWindow->GetChild(pWindow->GetChildCount() - 1); + } + if (pWindow && pWindow->GetType() == WindowType::FIXEDLINE) + { + if (bHide) + pWindow->Hide(); + else + pWindow->Show(); + } + + if (bHide) + { + sal_Int32 BoxId = 0; + while (BoxId <= m_pBox->GetChildCount() - 1) + { + if (m_pBox->GetChild(BoxId)) + { + pWindow = m_pBox->GetChild(BoxId); + ApplyBackground(pWindow); + } + BoxId++; + } + } + else + { + sal_Int32 BoxId = m_pBox->GetChildCount() - 1; + while (BoxId >= 0) + { + if (m_pBox->GetChild(BoxId)) + { + pWindow = m_pBox->GetChild(BoxId); + RemoveBackground(pWindow); + } + BoxId--; + } + } +} + +void NotebookbarPopup::dispose() +{ + PopupModeEnd(); + m_pBox.disposeAndClear(); + m_pParent.clear(); + + FloatingWindow::dispose(); +} + +void NotebookbarPopup::ApplyBackground(vcl::Window* pWindow) +{ + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + const BitmapEx& aPersona = rStyleSettings.GetPersonaHeader(); + + if (!aPersona.IsEmpty()) + pWindow->SetBackground(Wallpaper(aPersona)); + else + pWindow->SetBackground(rStyleSettings.GetDialogColor()); + + sal_Int32 nNext = 0; + VclPtr<vcl::Window> pChild = pWindow->GetChild(nNext); + while (pChild && pWindow->GetType() == WindowType::CONTAINER) + { + ApplyBackground(pChild); + nNext++; + if (pWindow->GetChild(nNext) && pWindow->GetType() == WindowType::CONTAINER) + pChild = pWindow->GetChild(nNext); + else + break; + } +} + +void NotebookbarPopup::RemoveBackground(vcl::Window* pWindow) +{ + pWindow->SetBackground(Wallpaper(COL_TRANSPARENT)); + + sal_Int32 nNext = 0; + VclPtr<vcl::Window> pChild = pWindow->GetChild(nNext); + while (pChild && pWindow->GetType() == WindowType::CONTAINER) + { + RemoveBackground(pChild); + nNext++; + if (pWindow->GetChild(nNext) && pWindow->GetType() == WindowType::CONTAINER) + pChild = pWindow->GetChild(nNext); + else + break; + } +} +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/PriorityHBox.cxx b/vcl/source/control/PriorityHBox.cxx new file mode 100644 index 000000000..c893cc833 --- /dev/null +++ b/vcl/source/control/PriorityHBox.cxx @@ -0,0 +1,197 @@ +/* -*- 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 <vcl/layout.hxx> +#include <PriorityHBox.hxx> +#include <comphelper/lok.hxx> + +namespace +{ +bool lcl_comparePriority(const vcl::IPrioritable* a, const vcl::IPrioritable* b) +{ + return a->GetPriority() < b->GetPriority(); +} +} + +PriorityHBox::PriorityHBox(vcl::Window* pParent) + : VclHBox(pParent) + , m_bInitialized(false) +{ +} + +PriorityHBox::~PriorityHBox() { disposeOnce(); } + +void PriorityHBox::Initialize() +{ + m_bInitialized = true; + + GetChildrenWithPriorities(); + SetSizeFromParent(); +} + +int PriorityHBox::GetHiddenCount() const +{ + int nCount = 0; + + for (auto pWindow : m_aSortedChildren) + if (pWindow->IsHidden()) + nCount++; + + return nCount; +} + +void PriorityHBox::SetSizeFromParent() +{ + vcl::Window* pParent = GetParent(); + if (pParent) + { + Size aParentSize = pParent->GetSizePixel(); + SetSizePixel(Size(aParentSize.getWidth(), aParentSize.getHeight())); + } +} + +Size PriorityHBox::calculateRequisition() const +{ + if (!m_bInitialized) + { + return VclHBox::calculateRequisition(); + } + + sal_uInt16 nVisibleChildren = 0; + + Size aSize; + for (vcl::Window* pChild = GetWindow(GetWindowType::FirstChild); pChild; + pChild = pChild->GetWindow(GetWindowType::Next)) + { + if (!pChild->IsVisible()) + continue; + ++nVisibleChildren; + Size aChildSize = getLayoutRequisition(*pChild); + + bool bAlwaysExpanded = true; + + vcl::IPrioritable* pPrioritable = dynamic_cast<vcl::IPrioritable*>(pChild); + if (pPrioritable && pPrioritable->GetPriority() != VCL_PRIORITY_DEFAULT) + bAlwaysExpanded = false; + + if (bAlwaysExpanded) + { + tools::Long nPrimaryDimension = getPrimaryDimension(aChildSize); + nPrimaryDimension += pChild->get_padding() * 2; + setPrimaryDimension(aChildSize, nPrimaryDimension); + } + else + setPrimaryDimension(aChildSize, 0); + + accumulateMaxes(aChildSize, aSize); + } + + return finalizeMaxes(aSize, nVisibleChildren); +} + +void PriorityHBox::Resize() +{ + if (!m_bInitialized) + Initialize(); + + if (!m_bInitialized || comphelper::LibreOfficeKit::isActive()) + { + return VclHBox::Resize(); + } + + tools::Long nWidth = GetSizePixel().Width(); + tools::Long nCurrentWidth = VclHBox::calculateRequisition().getWidth(); + + // Hide lower priority controls + for (vcl::IPrioritable* pPrioritable : m_aSortedChildren) + { + if (nCurrentWidth <= nWidth) + break; + + vcl::Window* pWindow = dynamic_cast<vcl::Window*>(pPrioritable); + + if (pWindow && pWindow->GetParent() == this) + { + nCurrentWidth -= pWindow->GetOutputSizePixel().Width() + get_spacing(); + pWindow->Show(); + pPrioritable->HideContent(); + nCurrentWidth += pWindow->GetOutputSizePixel().Width() + get_spacing(); + } + } + + auto pChildR = m_aSortedChildren.rbegin(); + // Show higher priority controls if we already have enough space + while (pChildR != m_aSortedChildren.rend()) + { + vcl::Window* pWindow = dynamic_cast<vcl::Window*>(*pChildR); + vcl::IPrioritable* pPrioritable = *pChildR; + + if (pWindow->GetParent() != this) + { + pChildR++; + continue; + } + + if (pWindow) + { + nCurrentWidth -= pWindow->GetOutputSizePixel().Width() + get_spacing(); + pWindow->Show(); + pPrioritable->ShowContent(); + nCurrentWidth += getLayoutRequisition(*pWindow).Width() + get_spacing(); + + if (nCurrentWidth > nWidth) + { + pPrioritable->HideContent(); + break; + } + } + + pChildR++; + } + + VclHBox::Resize(); +} + +void PriorityHBox::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + if (!m_bInitialized) + Initialize(); + + VclHBox::Paint(rRenderContext, rRect); +} + +void PriorityHBox::GetChildrenWithPriorities() +{ + for (sal_uInt16 i = 0; i < GetChildCount(); ++i) + { + vcl::Window* pChild = GetChild(i); + + // Add only containers which have explicitly assigned priority. + vcl::IPrioritable* pPrioritable = dynamic_cast<vcl::IPrioritable*>(pChild); + if (pPrioritable && pPrioritable->GetPriority() != VCL_PRIORITY_DEFAULT) + m_aSortedChildren.push_back(pPrioritable); + } + + if (m_aSortedChildren.empty()) + m_bInitialized = false; + + std::sort(m_aSortedChildren.begin(), m_aSortedChildren.end(), lcl_comparePriority); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/PriorityMergedHBox.cxx b/vcl/source/control/PriorityMergedHBox.cxx new file mode 100644 index 000000000..65d51cce3 --- /dev/null +++ b/vcl/source/control/PriorityMergedHBox.cxx @@ -0,0 +1,203 @@ +/* -*- 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 <vcl/toolkit/button.hxx> +#include <vcl/layout.hxx> +#include <bitmaps.hlst> +#include <NotebookbarPopup.hxx> +#include <PriorityHBox.hxx> +#include <PriorityMergedHBox.hxx> +#include <comphelper/lok.hxx> + +#define DUMMY_WIDTH 50 +#define BUTTON_WIDTH 30 + +/* +* PriorityMergedHBox is a VclHBox which hides its own children if there is no sufficient space. +*/ + +PriorityMergedHBox::PriorityMergedHBox(vcl::Window* pParent) + : PriorityHBox(pParent) +{ + m_pButton = VclPtr<PushButton>::Create(this, WB_FLATBUTTON); + m_pButton->SetClickHdl(LINK(this, PriorityMergedHBox, PBClickHdl)); + m_pButton->SetModeImage(Image(StockImage::Yes, CHEVRON)); + m_pButton->set_width_request(25); + m_pButton->set_pack_type(VclPackType::End); + m_pButton->Show(); +} + +void PriorityMergedHBox::Resize() +{ + if (comphelper::LibreOfficeKit::isActive()) + return VclHBox::Resize(); + + if (!m_bInitialized) + Initialize(); + + if (!m_bInitialized) + { + return VclHBox::Resize(); + } + + tools::Long nWidth = GetSizePixel().Width(); + tools::Long nCurrentWidth = VclHBox::calculateRequisition().getWidth() + BUTTON_WIDTH; + + // Hide lower priority controls + for (int i = GetChildCount() - 1; i >= 0; i--) + { + vcl::Window* pWindow = GetChild(i); + + if (nCurrentWidth <= nWidth) + break; + + if (pWindow && pWindow->GetParent() == this && pWindow->IsVisible()) + { + if (pWindow->GetOutputSizePixel().Width()) + nCurrentWidth -= pWindow->GetOutputSizePixel().Width(); + else + nCurrentWidth -= DUMMY_WIDTH; + pWindow->Hide(); + } + } + + // Show higher priority controls if we already have enough space + for (int i = 0; i < GetChildCount(); i++) + { + vcl::Window* pWindow = GetChild(i); + + if (pWindow->GetParent() != this) + { + continue; + } + + if (pWindow && !pWindow->IsVisible()) + { + pWindow->Show(); + nCurrentWidth += getLayoutRequisition(*pWindow).Width() + get_spacing(); + + if (nCurrentWidth > nWidth) + { + pWindow->Hide(); + break; + } + } + } + + VclHBox::Resize(); + + if (GetHiddenCount()) + m_pButton->Show(); + else + m_pButton->Hide(); +} + +void PriorityMergedHBox::dispose() +{ + m_pButton.disposeAndClear(); + if (m_pPopup) + m_pPopup.disposeAndClear(); + PriorityHBox::dispose(); +} + +int PriorityMergedHBox::GetHiddenCount() const +{ + int nCount = 0; + + for (int i = GetChildCount() - 1; i >= 0; i--) + { + vcl::Window* pWindow = GetChild(i); + if (pWindow && pWindow->GetParent() == this && !pWindow->IsVisible()) + nCount++; + } + + return nCount; +} + +Size PriorityMergedHBox::calculateRequisition() const +{ + if (!m_bInitialized) + { + return VclHBox::calculateRequisition(); + } + + sal_uInt16 nVisibleChildren = 0; + + Size aSize; + for (vcl::Window* pChild = GetWindow(GetWindowType::FirstChild); pChild; + pChild = pChild->GetWindow(GetWindowType::Next)) + { + if (!pChild->IsVisible()) + continue; + ++nVisibleChildren; + Size aChildSize = getLayoutRequisition(*pChild); + + tools::Long nPrimaryDimension = getPrimaryDimension(aChildSize); + nPrimaryDimension += pChild->get_padding() * 2; + setPrimaryDimension(aChildSize, nPrimaryDimension); + + accumulateMaxes(aChildSize, aSize); + } + + // find max height + for (vcl::Window* pChild = GetWindow(GetWindowType::FirstChild); pChild; + pChild = pChild->GetWindow(GetWindowType::Next)) + { + Size aChildSize = getLayoutRequisition(*pChild); + setPrimaryDimension(aChildSize, getPrimaryDimension(aSize)); + accumulateMaxes(aChildSize, aSize); + } + + setPrimaryDimension(aSize, 200); + return finalizeMaxes(aSize, nVisibleChildren); +} + +IMPL_LINK(PriorityMergedHBox, PBClickHdl, Button*, /*pButton*/, void) +{ + if (m_pPopup) + m_pPopup.disposeAndClear(); + + m_pPopup = VclPtr<NotebookbarPopup>::Create(this); + + for (int i = 0; i < GetChildCount(); i++) + { + vcl::Window* pWindow = GetChild(i); + if (pWindow != m_pButton) + { + if (!pWindow->IsVisible()) + { + pWindow->Show(); + pWindow->SetParent(m_pPopup->getBox()); + // count is decreased because we moved child + i--; + } + } + } + + m_pPopup->hideSeparators(true); + + tools::Long x = m_pButton->GetPosPixel().getX(); + tools::Long y = m_pButton->GetPosPixel().getY() + GetSizePixel().Height(); + tools::Rectangle aRect(x, y, x, y); + + m_pPopup->StartPopupMode(aRect, FloatWinPopupFlags::Down | FloatWinPopupFlags::GrabFocus + | FloatWinPopupFlags::AllMouseButtonClose); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/WeldedTabbedNotebookbar.cxx b/vcl/source/control/WeldedTabbedNotebookbar.cxx new file mode 100644 index 000000000..1a3311de9 --- /dev/null +++ b/vcl/source/control/WeldedTabbedNotebookbar.cxx @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <vcl/WeldedTabbedNotebookbar.hxx> +#include <vcl/svapp.hxx> +#include <jsdialog/jsdialogbuilder.hxx> + +WeldedTabbedNotebookbar::WeldedTabbedNotebookbar( + const VclPtr<vcl::Window>& pContainerWindow, const OUString& rUIFilePath, + const css::uno::Reference<css::frame::XFrame>& rFrame, sal_uInt64 nWindowId) + : m_xBuilder(JSInstanceBuilder::CreateNotebookbarBuilder( + pContainerWindow, AllSettings::GetUIRootDir(), rUIFilePath, rFrame, nWindowId)) +{ + m_xContainer = m_xBuilder->weld_container("NotebookBar"); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/button.cxx b/vcl/source/control/button.cxx new file mode 100644 index 000000000..210d2b629 --- /dev/null +++ b/vcl/source/control/button.cxx @@ -0,0 +1,3826 @@ +/* -*- 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 <tools/poly.hxx> + +#include <vcl/builder.hxx> +#include <vcl/cvtgrf.hxx> +#include <vcl/image.hxx> +#include <vcl/bitmapex.hxx> +#include <vcl/decoview.hxx> +#include <vcl/event.hxx> +#include <vcl/svapp.hxx> +#include <vcl/settings.hxx> +#include <vcl/toolkit/dialog.hxx> +#include <vcl/toolkit/fixed.hxx> +#include <vcl/toolkit/button.hxx> +#include <vcl/salnativewidgets.hxx> +#include <vcl/toolkit/edit.hxx> +#include <vcl/layout.hxx> +#include <vcl/stdtext.hxx> +#include <vcl/uitest/uiobject.hxx> + +#include <bitmaps.hlst> +#include <svdata.hxx> +#include <window.h> +#include <vclstatuslistener.hxx> +#include <osl/diagnose.h> + +#include <comphelper/base64.hxx> +#include <comphelper/dispatchcommand.hxx> +#include <comphelper/lok.hxx> +#include <officecfg/Office/Common.hxx> +#include <boost/property_tree/ptree.hpp> +#include <tools/json_writer.hxx> +#include <tools/stream.hxx> + + +using namespace css; + +constexpr auto PUSHBUTTON_VIEW_STYLE = WB_3DLOOK | + WB_LEFT | WB_CENTER | WB_RIGHT | + WB_TOP | WB_VCENTER | WB_BOTTOM | + WB_WORDBREAK | WB_NOLABEL | + WB_DEFBUTTON | WB_NOLIGHTBORDER | + WB_RECTSTYLE | WB_SMALLSTYLE | + WB_TOGGLE; +constexpr auto RADIOBUTTON_VIEW_STYLE = WB_3DLOOK | + WB_LEFT | WB_CENTER | WB_RIGHT | + WB_TOP | WB_VCENTER | WB_BOTTOM | + WB_WORDBREAK | WB_NOLABEL; +constexpr auto CHECKBOX_VIEW_STYLE = WB_3DLOOK | + WB_LEFT | WB_CENTER | WB_RIGHT | + WB_TOP | WB_VCENTER | WB_BOTTOM | + WB_WORDBREAK | WB_NOLABEL; + +#define STYLE_RADIOBUTTON_MONO (sal_uInt16(0x0001)) // legacy +#define STYLE_CHECKBOX_MONO (sal_uInt16(0x0001)) // legacy + +class ImplCommonButtonData +{ +public: + ImplCommonButtonData(); + + tools::Rectangle maFocusRect; + tools::Long mnSeparatorX; + DrawButtonFlags mnButtonState; + bool mbSmallSymbol; + bool mbGeneratedTooltip; + + Image maImage; + ImageAlign meImageAlign; + SymbolAlign meSymbolAlign; + + Image maCustomContentImage; + + /** StatusListener. Updates the button as the slot state changes */ + rtl::Reference<VclStatusListener<Button>> mpStatusListener; +}; + +ImplCommonButtonData::ImplCommonButtonData() : mnSeparatorX(0), mnButtonState(DrawButtonFlags::NONE), +mbSmallSymbol(false), mbGeneratedTooltip(false), meImageAlign(ImageAlign::Top), meSymbolAlign(SymbolAlign::LEFT) +{ +} + +Button::Button( WindowType nType ) : + Control( nType ), + mpButtonData( std::make_unique<ImplCommonButtonData>() ) +{ +} + +Button::~Button() +{ + disposeOnce(); +} + +void Button::dispose() +{ + if (mpButtonData->mpStatusListener.is()) + mpButtonData->mpStatusListener->dispose(); + Control::dispose(); +} + +void Button::SetCommandHandler(const OUString& aCommand) +{ + maCommand = aCommand; + SetClickHdl( LINK( this, Button, dispatchCommandHandler) ); + + mpButtonData->mpStatusListener = new VclStatusListener<Button>(this, aCommand); + mpButtonData->mpStatusListener->startListening(); +} + +void Button::Click() +{ + ImplCallEventListenersAndHandler( VclEventId::ButtonClick, [this] () { maClickHdl.Call(this); } ); +} + +void Button::SetModeImage( const Image& rImage ) +{ + if ( rImage != mpButtonData->maImage ) + { + mpButtonData->maImage = rImage; + StateChanged( StateChangedType::Data ); + queue_resize(); + } +} + +Image const & Button::GetModeImage( ) const +{ + return mpButtonData->maImage; +} + +bool Button::HasImage() const +{ + return !!(mpButtonData->maImage); +} + +void Button::SetImageAlign( ImageAlign eAlign ) +{ + if ( mpButtonData->meImageAlign != eAlign ) + { + mpButtonData->meImageAlign = eAlign; + StateChanged( StateChangedType::Data ); + } +} + +ImageAlign Button::GetImageAlign() const +{ + return mpButtonData->meImageAlign; +} + +void Button::SetCustomButtonImage(const Image& rImage) +{ + if (rImage != mpButtonData->maCustomContentImage) + { + mpButtonData->maCustomContentImage = rImage; + StateChanged( StateChangedType::Data ); + } +} + +Image const & Button::GetCustomButtonImage() const +{ + return mpButtonData->maCustomContentImage; +} + +tools::Long Button::ImplGetSeparatorX() const +{ + return mpButtonData->mnSeparatorX; +} + +void Button::ImplSetSeparatorX( tools::Long nX ) +{ + mpButtonData->mnSeparatorX = nX; +} + +DrawTextFlags Button::ImplGetTextStyle( WinBits nWinStyle, SystemTextColorFlags nSystemTextColorFlags ) const +{ + const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); + DrawTextFlags nTextStyle = FixedText::ImplGetTextStyle(nWinStyle & ~WB_DEFBUTTON); + + if (!IsEnabled()) + nTextStyle |= DrawTextFlags::Disable; + + if ((nSystemTextColorFlags & SystemTextColorFlags::Mono) || + (rStyleSettings.GetOptions() & StyleSettingsOptions::Mono)) + { + nTextStyle |= DrawTextFlags::Mono; + } + + return nTextStyle; +} + +void Button::ImplDrawAlignedImage(OutputDevice* pDev, Point& rPos, + Size& rSize, + sal_Int32 nImageSep, + DrawTextFlags nTextStyle, tools::Rectangle *pSymbolRect, + bool bAddImageSep) +{ + OUString aText(GetText()); + bool bDrawImage = HasImage(); + bool bDrawText = !aText.isEmpty(); + bool bHasSymbol = pSymbolRect != nullptr; + + // No text and no image => nothing to do => return + if (!bDrawImage && !bDrawText && !bHasSymbol) + return; + + WinBits nWinStyle = GetStyle(); + tools::Rectangle aOutRect( rPos, rSize ); + ImageAlign eImageAlign = mpButtonData->meImageAlign; + Size aImageSize = mpButtonData->maImage.GetSizePixel(); + + aImageSize.setWidth( CalcZoom( aImageSize.Width() ) ); + aImageSize.setHeight( CalcZoom( aImageSize.Height() ) ); + + // Drawing text or symbol only is simple, use style and output rectangle + if (bHasSymbol && !bDrawImage && !bDrawText) + { + *pSymbolRect = aOutRect; + return; + } + else if (bDrawText && !bDrawImage && !bHasSymbol) + { + aOutRect = DrawControlText(*pDev, aOutRect, aText, nTextStyle, nullptr, nullptr); + tools::Rectangle textRect = GetTextRect( + tools::Rectangle(Point(), Size(0x7fffffff, 0x7fffffff)), aText, nTextStyle); + // If the button text doesn't fit into it, put it into a tooltip (might happen in sidebar) + if (GetQuickHelpText()!= aText && mpButtonData->mbGeneratedTooltip) + SetQuickHelpText(""); + if (GetQuickHelpText().isEmpty() && textRect.getWidth() > rSize.getWidth()) + { + SetQuickHelpText(aText); + mpButtonData->mbGeneratedTooltip = true; + } + + ImplSetFocusRect(aOutRect); + rSize = aOutRect.GetSize(); + rPos = aOutRect.TopLeft(); + + return; + } + + // check for HC mode ( image only! ) + Image* pImage = &(mpButtonData->maImage); + + Size aTextSize; + Size aSymbolSize; + Size aDeviceTextSize; + Point aImagePos = rPos; + Point aTextPos = rPos; + tools::Rectangle aUnion(aImagePos, aImageSize); + tools::Long nSymbolHeight = 0; + + if (bDrawText || bHasSymbol) + { + // Get the size of the text output area ( the symbol will be drawn in + // this area as well, so the symbol rectangle will be calculated here, too ) + + tools::Rectangle aRect(Point(), rSize); + Size aTSSize; + + if (bHasSymbol) + { + tools::Rectangle aSymbol; + if (bDrawText) + { + nSymbolHeight = pDev->GetTextHeight(); + if (mpButtonData->mbSmallSymbol) + nSymbolHeight = nSymbolHeight * 3 / 4; + + aSymbol = tools::Rectangle(Point(), Size(nSymbolHeight, nSymbolHeight)); + ImplCalcSymbolRect(aSymbol); + aRect.AdjustLeft(3 * nSymbolHeight / 2 ); + aTSSize.setWidth( 3 * nSymbolHeight / 2 ); + } + else + { + aSymbol = tools::Rectangle(Point(), rSize); + ImplCalcSymbolRect(aSymbol); + aTSSize.setWidth( aSymbol.GetWidth() ); + } + aTSSize.setHeight( aSymbol.GetHeight() ); + aSymbolSize = aSymbol.GetSize(); + } + + if (bDrawText) + { + if ((eImageAlign == ImageAlign::LeftTop) || + (eImageAlign == ImageAlign::Left ) || + (eImageAlign == ImageAlign::LeftBottom) || + (eImageAlign == ImageAlign::RightTop) || + (eImageAlign == ImageAlign::Right) || + (eImageAlign == ImageAlign::RightBottom)) + { + aRect.AdjustRight( -sal_Int32(aImageSize.Width() + nImageSep) ); + } + else if ((eImageAlign == ImageAlign::TopLeft) || + (eImageAlign == ImageAlign::Top) || + (eImageAlign == ImageAlign::TopRight) || + (eImageAlign == ImageAlign::BottomLeft) || + (eImageAlign == ImageAlign::Bottom) || + (eImageAlign == ImageAlign::BottomRight)) + { + aRect.AdjustBottom( -sal_Int32(aImageSize.Height() + nImageSep) ); + } + + aRect = GetControlTextRect(*pDev, aRect, aText, nTextStyle, &aDeviceTextSize); + aTextSize = aRect.GetSize(); + + aTSSize.AdjustWidth(aTextSize.Width() ); + + if (aTSSize.Height() < aTextSize.Height()) + aTSSize.setHeight( aTextSize.Height() ); + + if (bAddImageSep && bDrawImage) + { + tools::Long nDiff = (aImageSize.Height() - aTextSize.Height()) / 3; + if (nDiff > 0) + nImageSep += nDiff; + } + } + + Size aMax; + aMax.setWidth( std::max(aTSSize.Width(), aImageSize.Width()) ); + aMax.setHeight( std::max(aTSSize.Height(), aImageSize.Height()) ); + + // Now calculate the output area for the image and the text according to the image align flags + + if ((eImageAlign == ImageAlign::Left) || + (eImageAlign == ImageAlign::Right)) + { + aImagePos.setY( rPos.Y() + (aMax.Height() - aImageSize.Height()) / 2 ); + aTextPos.setY( rPos.Y() + (aMax.Height() - aTSSize.Height()) / 2 ); + } + else if ((eImageAlign == ImageAlign::LeftBottom) || + (eImageAlign == ImageAlign::RightBottom)) + { + aImagePos.setY( rPos.Y() + aMax.Height() - aImageSize.Height() ); + aTextPos.setY( rPos.Y() + aMax.Height() - aTSSize.Height() ); + } + else if ((eImageAlign == ImageAlign::Top) || + (eImageAlign == ImageAlign::Bottom)) + { + aImagePos.setX( rPos.X() + (aMax.Width() - aImageSize.Width()) / 2 ); + aTextPos.setX( rPos.X() + (aMax.Width() - aTSSize.Width()) / 2 ); + } + else if ((eImageAlign == ImageAlign::TopRight) || + (eImageAlign == ImageAlign::BottomRight)) + { + aImagePos.setX( rPos.X() + aMax.Width() - aImageSize.Width() ); + aTextPos.setX( rPos.X() + aMax.Width() - aTSSize.Width() ); + } + + if ((eImageAlign == ImageAlign::LeftTop) || + (eImageAlign == ImageAlign::Left) || + (eImageAlign == ImageAlign::LeftBottom)) + { + aTextPos.setX( rPos.X() + aImageSize.Width() + nImageSep ); + } + else if ((eImageAlign == ImageAlign::RightTop) || + (eImageAlign == ImageAlign::Right) || + (eImageAlign == ImageAlign::RightBottom)) + { + aImagePos.setX( rPos.X() + aTSSize.Width() + nImageSep ); + } + else if ((eImageAlign == ImageAlign::TopLeft) || + (eImageAlign == ImageAlign::Top) || + (eImageAlign == ImageAlign::TopRight)) + { + aTextPos.setY( rPos.Y() + aImageSize.Height() + nImageSep ); + } + else if ((eImageAlign == ImageAlign::BottomLeft) || + (eImageAlign == ImageAlign::Bottom) || + (eImageAlign == ImageAlign::BottomRight)) + { + aImagePos.setY( rPos.Y() + aTSSize.Height() + nImageSep ); + } + else if (eImageAlign == ImageAlign::Center) + { + aImagePos.setX( rPos.X() + (aMax.Width() - aImageSize.Width()) / 2 ); + aImagePos.setY( rPos.Y() + (aMax.Height() - aImageSize.Height()) / 2 ); + aTextPos.setX( rPos.X() + (aMax.Width() - aTSSize.Width()) / 2 ); + aTextPos.setY( rPos.Y() + (aMax.Height() - aTSSize.Height()) / 2 ); + } + aUnion = tools::Rectangle(aImagePos, aImageSize); + aUnion.Union(tools::Rectangle(aTextPos, aTSSize)); + } + + // Now place the combination of text and image in the output area of the button + // according to the window style (WinBits) + tools::Long nXOffset = 0; + tools::Long nYOffset = 0; + + if (nWinStyle & WB_CENTER) + { + nXOffset = (rSize.Width() - aUnion.GetWidth()) / 2; + } + else if (nWinStyle & WB_RIGHT) + { + nXOffset = rSize.Width() - aUnion.GetWidth(); + } + + if (nWinStyle & WB_VCENTER) + { + nYOffset = (rSize.Height() - aUnion.GetHeight()) / 2; + } + else if (nWinStyle & WB_BOTTOM) + { + nYOffset = rSize.Height() - aUnion.GetHeight(); + } + + // the top left corner should always be visible, so we don't allow negative offsets + if (nXOffset < 0) nXOffset = 0; + if (nYOffset < 0) nYOffset = 0; + + aImagePos.AdjustX(nXOffset ); + aImagePos.AdjustY(nYOffset ); + aTextPos.AdjustX(nXOffset ); + aTextPos.AdjustY(nYOffset ); + + // set rPos and rSize to the union + rSize = aUnion.GetSize(); + rPos.AdjustX(nXOffset ); + rPos.AdjustY(nYOffset ); + + if (bHasSymbol) + { + if (mpButtonData->meSymbolAlign == SymbolAlign::RIGHT) + { + Point aRightPos(aTextPos.X() + aTextSize.Width() + aSymbolSize.Width() / 2, aTextPos.Y()); + *pSymbolRect = tools::Rectangle(aRightPos, aSymbolSize); + } + else + { + *pSymbolRect = tools::Rectangle(aTextPos, aSymbolSize); + aTextPos.AdjustX(3 * nSymbolHeight / 2 ); + } + if (mpButtonData->mbSmallSymbol) + { + nYOffset = (aUnion.GetHeight() - aSymbolSize.Height()) / 2; + pSymbolRect->SetPosY(aTextPos.Y() + nYOffset); + } + } + + DrawImageFlags nStyle = DrawImageFlags::NONE; + + if (!IsEnabled()) + { + nStyle |= DrawImageFlags::Disable; + } + + if (IsZoom()) + pDev->DrawImage(aImagePos, aImageSize, *pImage, nStyle); + else + pDev->DrawImage(aImagePos, *pImage, nStyle); + + if (bDrawText) + { + const tools::Rectangle aTOutRect(aTextPos, aTextSize); + ImplSetFocusRect(aTOutRect); + DrawControlText(*pDev, aTOutRect, aText, nTextStyle, nullptr, nullptr, &aDeviceTextSize); + } + else + { + ImplSetFocusRect(tools::Rectangle(aImagePos, aImageSize)); + } +} + +void Button::ImplSetFocusRect(const tools::Rectangle &rFocusRect) +{ + tools::Rectangle aFocusRect = rFocusRect; + tools::Rectangle aOutputRect(Point(), GetOutputSizePixel()); + + if (!aFocusRect.IsEmpty()) + { + aFocusRect.AdjustLeft( -1 ); + aFocusRect.AdjustTop( -1 ); + aFocusRect.AdjustRight( 1 ); + aFocusRect.AdjustBottom( 1 ); + } + + if (aFocusRect.Left() < aOutputRect.Left()) + aFocusRect.SetLeft( aOutputRect.Left() ); + if (aFocusRect.Top() < aOutputRect.Top()) + aFocusRect.SetTop( aOutputRect.Top() ); + if (aFocusRect.Right() > aOutputRect.Right()) + aFocusRect.SetRight( aOutputRect.Right() ); + if (aFocusRect.Bottom() > aOutputRect.Bottom()) + aFocusRect.SetBottom( aOutputRect.Bottom() ); + + mpButtonData->maFocusRect = aFocusRect; +} + +const tools::Rectangle& Button::ImplGetFocusRect() const +{ + return mpButtonData->maFocusRect; +} + +DrawButtonFlags& Button::GetButtonState() +{ + return mpButtonData->mnButtonState; +} + +DrawButtonFlags Button::GetButtonState() const +{ + return mpButtonData->mnButtonState; +} + +void Button::ImplSetSymbolAlign( SymbolAlign eAlign ) +{ + if ( mpButtonData->meSymbolAlign != eAlign ) + { + mpButtonData->meSymbolAlign = eAlign; + StateChanged( StateChangedType::Data ); + } +} + +void Button::SetSmallSymbol() +{ + mpButtonData->mbSmallSymbol = true; +} + +bool Button::IsSmallSymbol () const +{ + return mpButtonData->mbSmallSymbol; +} + +bool Button::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "image-position") + { + ImageAlign eAlign = ImageAlign::Left; + if (rValue == "left") + eAlign = ImageAlign::Left; + else if (rValue == "right") + eAlign = ImageAlign::Right; + else if (rValue == "top") + eAlign = ImageAlign::Top; + else if (rValue == "bottom") + eAlign = ImageAlign::Bottom; + SetImageAlign(eAlign); + } + else if (rKey == "focus-on-click") + { + WinBits nBits = GetStyle(); + nBits &= ~WB_NOPOINTERFOCUS; + if (!toBool(rValue)) + nBits |= WB_NOPOINTERFOCUS; + SetStyle(nBits); + } + else + return Control::set_property(rKey, rValue); + return true; +} + +void Button::statusChanged(const css::frame::FeatureStateEvent& rEvent) +{ + Enable(rEvent.IsEnabled); +} + +FactoryFunction Button::GetUITestFactory() const +{ + return ButtonUIObject::create; +} + +namespace +{ + +const char* symbolTypeName(SymbolType eSymbolType) +{ + switch (eSymbolType) + { + case SymbolType::DONTKNOW: return "DONTKNOW"; + case SymbolType::IMAGE: return "IMAGE"; + case SymbolType::ARROW_UP: return "ARROW_UP"; + case SymbolType::ARROW_DOWN: return "ARROW_DOWN"; + case SymbolType::ARROW_LEFT: return "ARROW_LEFT"; + case SymbolType::ARROW_RIGHT: return "ARROW_RIGHT"; + case SymbolType::SPIN_UP: return "SPIN_UP"; + case SymbolType::SPIN_DOWN: return "SPIN_DOWN"; + case SymbolType::SPIN_LEFT: return "SPIN_LEFT"; + case SymbolType::SPIN_RIGHT: return "SPIN_RIGHT"; + case SymbolType::FIRST: return "FIRST"; + case SymbolType::LAST: return "LAST"; + case SymbolType::PREV: return "PREV"; + case SymbolType::NEXT: return "NEXT"; + case SymbolType::PAGEUP: return "PAGEUP"; + case SymbolType::PAGEDOWN: return "PAGEDOWN"; + case SymbolType::PLAY: return "PLAY"; + case SymbolType::STOP: return "STOP"; + case SymbolType::CLOSE: return "CLOSE"; + case SymbolType::CHECKMARK: return "CHECKMARK"; + case SymbolType::RADIOCHECKMARK: return "RADIOCHECKMARK"; + case SymbolType::FLOAT: return "FLOAT"; + case SymbolType::DOCK: return "DOCK"; + case SymbolType::HIDE: return "HIDE"; + case SymbolType::HELP: return "HELP"; + case SymbolType::PLUS: return "PLUS"; + } + + return "UNKNOWN"; +} + +} + +void Button::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Control::DumpAsPropertyTree(rJsonWriter); + rJsonWriter.put("text", GetText()); + if (HasImage()) + { + SvMemoryStream aOStm(6535, 6535); + if(GraphicConverter::Export(aOStm, GetModeImage().GetBitmapEx(), ConvertDataFormat::PNG) == ERRCODE_NONE) + { + css::uno::Sequence<sal_Int8> aSeq( static_cast<sal_Int8 const *>(aOStm.GetData()), aOStm.Tell()); + OUStringBuffer aBuffer("data:image/png;base64,"); + ::comphelper::Base64::encode(aBuffer, aSeq); + rJsonWriter.put("image", aBuffer.makeStringAndClear()); + } + } + + if (GetStyle() & WB_DEFBUTTON) + rJsonWriter.put("has_default", true); +} + +void PushButton::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Button::DumpAsPropertyTree(rJsonWriter); + if (GetSymbol() != SymbolType::DONTKNOW) + rJsonWriter.put("symbol", symbolTypeName(GetSymbol())); +} + +IMPL_STATIC_LINK( Button, dispatchCommandHandler, Button*, pButton, void ) +{ + if (pButton == nullptr) + return; + + comphelper::dispatchCommand(pButton->maCommand, uno::Sequence<beans::PropertyValue>()); +} + +void PushButton::ImplInitPushButtonData() +{ + mpWindowImpl->mbPushButton = true; + + meSymbol = SymbolType::DONTKNOW; + meState = TRISTATE_FALSE; + mnDDStyle = PushButtonDropdownStyle::NONE; + mbIsActive = false; + mbPressed = false; + mbIsAction = false; +} + +namespace +{ + vcl::Window* getPreviousSibling(vcl::Window const *pParent) + { + return pParent ? pParent->GetWindow(GetWindowType::LastChild) : nullptr; + } +} + +void PushButton::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle(getPreviousSibling(pParent), nStyle); + Button::ImplInit( pParent, nStyle, nullptr ); + + if ( nStyle & WB_NOLIGHTBORDER ) + GetButtonState() |= DrawButtonFlags::NoLightBorder; + + ImplInitSettings( true ); +} + +WinBits PushButton::ImplInitStyle( const vcl::Window* pPrevWindow, WinBits nStyle ) +{ + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + + // if no alignment is given, default to "vertically centered". This is because since + // #i26046#, we respect the vertical alignment flags (previously we didn't completely), + // but we of course want to look as before when no vertical alignment is specified + if ( ( nStyle & ( WB_TOP | WB_VCENTER | WB_BOTTOM ) ) == 0 ) + nStyle |= WB_VCENTER; + + if ( !(nStyle & WB_NOGROUP) && + (!pPrevWindow || + ((pPrevWindow->GetType() != WindowType::PUSHBUTTON ) && + (pPrevWindow->GetType() != WindowType::OKBUTTON ) && + (pPrevWindow->GetType() != WindowType::CANCELBUTTON) && + (pPrevWindow->GetType() != WindowType::HELPBUTTON )) ) ) + nStyle |= WB_GROUP; + return nStyle; +} + +const vcl::Font& PushButton::GetCanonicalFont( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetPushButtonFont(); +} + +const Color& PushButton::GetCanonicalTextColor( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetButtonTextColor(); +} + +void PushButton::ImplInitSettings( bool bBackground ) +{ + Button::ImplInitSettings(); + + if ( !bBackground ) + return; + + SetBackground(); + // #i38498#: do not check for GetParent()->IsChildTransparentModeEnabled() + // otherwise the formcontrol button will be overdrawn due to ParentClipMode::NoClip + // for radio and checkbox this is ok as they should appear transparent in documents + if ( IsNativeControlSupported( ControlType::Pushbutton, ControlPart::Entire ) || + (GetStyle() & WB_FLATBUTTON) != 0 ) + { + EnableChildTransparentMode(); + SetParentClipMode( ParentClipMode::NoClip ); + SetPaintTransparent( true ); + + if ((GetStyle() & WB_FLATBUTTON) == 0) + mpWindowImpl->mbUseNativeFocus = ImplGetSVData()->maNWFData.mbNoFocusRects; + else + mpWindowImpl->mbUseNativeFocus = ImplGetSVData()->maNWFData.mbNoFocusRectsForFlatButtons; + } + else + { + EnableChildTransparentMode( false ); + SetParentClipMode(); + SetPaintTransparent( false ); + } +} + +void PushButton::ImplDrawPushButtonFrame(vcl::RenderContext& rRenderContext, + tools::Rectangle& rRect, DrawButtonFlags nStyle) +{ + if (!(GetStyle() & (WB_RECTSTYLE | WB_SMALLSTYLE))) + { + StyleSettings aStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + if (IsControlBackground()) + aStyleSettings.Set3DColors(GetControlBackground()); + } + + DecorationView aDecoView(&rRenderContext); + if (IsControlBackground()) + { + AllSettings aSettings = rRenderContext.GetSettings(); + AllSettings aOldSettings = aSettings; + StyleSettings aStyleSettings = aSettings.GetStyleSettings(); + if (nStyle & DrawButtonFlags::Highlight) + { + // with the custom background, native highlight do nothing, so code below mimic + // native highlight by changing luminance + Color controlBackgroundColorHighlighted = GetControlBackground(); + sal_uInt8 colorLuminance = controlBackgroundColorHighlighted.GetLuminance(); + if (colorLuminance < 205) + controlBackgroundColorHighlighted.IncreaseLuminance(50); + else + controlBackgroundColorHighlighted.DecreaseLuminance(50); + aStyleSettings.Set3DColors(controlBackgroundColorHighlighted); + } + else + aStyleSettings.Set3DColors(GetControlBackground()); + aSettings.SetStyleSettings(aStyleSettings); + + // Call OutputDevice::SetSettings() explicitly, as rRenderContext may + // be a vcl::Window in fact, and vcl::Window::SetSettings() will call + // Invalidate(), which is a problem, since we're in Paint(). + rRenderContext.OutputDevice::SetSettings(aSettings); + rRect = aDecoView.DrawButton(rRect, nStyle); + rRenderContext.OutputDevice::SetSettings(aOldSettings); + } + else + rRect = aDecoView.DrawButton(rRect, nStyle); +} + +bool PushButton::ImplHitTestPushButton( vcl::Window const * pDev, + const Point& rPos ) +{ + tools::Rectangle aTestRect( Point(), pDev->GetOutputSizePixel() ); + + return aTestRect.Contains( rPos ); +} + +DrawTextFlags PushButton::ImplGetTextStyle( SystemTextColorFlags nSystemTextColorFlags ) const +{ + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + + DrawTextFlags nTextStyle = DrawTextFlags::Mnemonic | DrawTextFlags::MultiLine | DrawTextFlags::EndEllipsis; + + if ( ( rStyleSettings.GetOptions() & StyleSettingsOptions::Mono ) || + ( nSystemTextColorFlags & SystemTextColorFlags::Mono ) ) + nTextStyle |= DrawTextFlags::Mono; + + if ( GetStyle() & WB_WORDBREAK ) + nTextStyle |= DrawTextFlags::WordBreak; + if ( GetStyle() & WB_NOLABEL ) + nTextStyle &= ~DrawTextFlags::Mnemonic; + + if ( GetStyle() & WB_LEFT ) + nTextStyle |= DrawTextFlags::Left; + else if ( GetStyle() & WB_RIGHT ) + nTextStyle |= DrawTextFlags::Right; + else + nTextStyle |= DrawTextFlags::Center; + + if ( GetStyle() & WB_TOP ) + nTextStyle |= DrawTextFlags::Top; + else if ( GetStyle() & WB_BOTTOM ) + nTextStyle |= DrawTextFlags::Bottom; + else + nTextStyle |= DrawTextFlags::VCenter; + + if ( !IsEnabled() ) + nTextStyle |= DrawTextFlags::Disable; + + return nTextStyle; +} + +void PushButton::ImplDrawPushButtonContent(OutputDevice *pDev, SystemTextColorFlags nSystemTextColorFlags, + const tools::Rectangle &rRect, bool bMenuBtnSep, + DrawButtonFlags nButtonFlags) +{ + const StyleSettings &rStyleSettings = GetSettings().GetStyleSettings(); + tools::Rectangle aInRect = rRect; + Color aColor; + DrawTextFlags nTextStyle = ImplGetTextStyle(nSystemTextColorFlags); + DrawSymbolFlags nStyle; + + if (aInRect.Right() < aInRect.Left() || aInRect.Bottom() < aInRect.Top()) + return; + + pDev->Push(vcl::PushFlags::CLIPREGION); + pDev->IntersectClipRegion(aInRect); + + if (nSystemTextColorFlags & SystemTextColorFlags::Mono) + aColor = COL_BLACK; + + else if (IsControlForeground()) + aColor = GetControlForeground(); + + // Button types with possibly different text coloring are flat buttons and regular buttons. Regular buttons may be action + // buttons and may have an additional default status. Moreover all buttons may have an additional pressed and rollover + // (highlight) status. Pressed buttons are always in rollover status. + + else if (GetStyle() & WB_FLATBUTTON) + if (nButtonFlags & DrawButtonFlags::Pressed) + aColor = rStyleSettings.GetFlatButtonPressedRolloverTextColor(); + else if (nButtonFlags & DrawButtonFlags::Highlight) + aColor = rStyleSettings.GetFlatButtonRolloverTextColor(); + else + aColor = rStyleSettings.GetFlatButtonTextColor(); + else + if (isAction() && (nButtonFlags & DrawButtonFlags::Default)) + if (nButtonFlags & DrawButtonFlags::Pressed) + aColor = rStyleSettings.GetDefaultActionButtonPressedRolloverTextColor(); + else if (nButtonFlags & DrawButtonFlags::Highlight) + aColor = rStyleSettings.GetDefaultActionButtonRolloverTextColor(); + else + aColor = rStyleSettings.GetDefaultActionButtonTextColor(); + else if (isAction()) + if (nButtonFlags & DrawButtonFlags::Pressed) + aColor = rStyleSettings.GetActionButtonPressedRolloverTextColor(); + else if (nButtonFlags & DrawButtonFlags::Highlight) + aColor = rStyleSettings.GetActionButtonRolloverTextColor(); + else + aColor = rStyleSettings.GetActionButtonTextColor(); + else if (nButtonFlags & DrawButtonFlags::Default) + if (nButtonFlags & DrawButtonFlags::Pressed) + aColor = rStyleSettings.GetDefaultButtonPressedRolloverTextColor(); + else if (nButtonFlags & DrawButtonFlags::Highlight) + aColor = rStyleSettings.GetDefaultButtonRolloverTextColor(); + else + aColor = rStyleSettings.GetDefaultButtonTextColor(); + else + if (nButtonFlags & DrawButtonFlags::Pressed) + aColor = rStyleSettings.GetButtonPressedRolloverTextColor(); + else if (nButtonFlags & DrawButtonFlags::Highlight) + aColor = rStyleSettings.GetButtonRolloverTextColor(); + else + aColor = rStyleSettings.GetButtonTextColor(); + + pDev->SetTextColor(aColor); + + if ( IsEnabled() ) + nStyle = DrawSymbolFlags::NONE; + else + nStyle = DrawSymbolFlags::Disable; + + Size aSize = rRect.GetSize(); + Point aPos = rRect.TopLeft(); + + sal_Int32 nImageSep = 1 + (pDev->GetTextHeight()-10)/2; + if( nImageSep < 1 ) + nImageSep = 1; + if ( mnDDStyle == PushButtonDropdownStyle::MenuButton || + mnDDStyle == PushButtonDropdownStyle::SplitMenuButton ) + { + tools::Long nSeparatorX = 0; + tools::Rectangle aSymbolRect = aInRect; + + // calculate symbol size + tools::Long nSymbolSize = pDev->GetTextHeight() / 2 + 1; + if (nSymbolSize > aSize.Width() / 2) + nSymbolSize = aSize.Width() / 2; + + nSeparatorX = aInRect.Right() - 2*nSymbolSize; + + // tdf#141761 Minimum width should be (1) Pixel, see comment + // with same task number above for more info + const tools::Long nWidthAdjust(2*nSymbolSize); + aSize.setWidth(std::max(static_cast<tools::Long>(1), aSize.getWidth() - nWidthAdjust)); + + // center symbol rectangle in the separated area + aSymbolRect.AdjustRight( -(nSymbolSize/2) ); + aSymbolRect.SetLeft( aSymbolRect.Right() - nSymbolSize ); + + ImplDrawAlignedImage( pDev, aPos, aSize, nImageSep, + nTextStyle, nullptr, true ); + + tools::Long nDistance = (aSymbolRect.GetHeight() > 10) ? 2 : 1; + DecorationView aDecoView( pDev ); + if( bMenuBtnSep && nSeparatorX > 0 ) + { + Point aStartPt( nSeparatorX, aSymbolRect.Top()+nDistance ); + Point aEndPt( nSeparatorX, aSymbolRect.Bottom()-nDistance ); + aDecoView.DrawSeparator( aStartPt, aEndPt ); + } + ImplSetSeparatorX( nSeparatorX ); + + aDecoView.DrawSymbol( aSymbolRect, SymbolType::SPIN_DOWN, aColor, nStyle ); + + } + else + { + tools::Rectangle aSymbolRect; + ImplDrawAlignedImage( pDev, aPos, aSize, nImageSep, + nTextStyle, IsSymbol() ? &aSymbolRect : nullptr, true ); + + if ( IsSymbol() ) + { + DecorationView aDecoView( pDev ); + aDecoView.DrawSymbol( aSymbolRect, meSymbol, aColor, nStyle ); + } + } + + pDev->Pop(); // restore clipregion +} + +void PushButton::ImplDrawPushButton(vcl::RenderContext& rRenderContext) +{ + HideFocus(); + + DrawButtonFlags nButtonStyle = GetButtonState(); + Size aOutSz(GetOutputSizePixel()); + tools::Rectangle aRect(Point(), aOutSz); + tools::Rectangle aInRect = aRect; + bool bNativeOK = false; + + // adjust style if button should be rendered 'pressed' + if (mbPressed || mbIsActive) + nButtonStyle |= DrawButtonFlags::Pressed; + + // TODO: move this to Window class or make it a member !!! + ControlType aCtrlType = ControlType::Generic; + switch(GetParent()->GetType()) + { + case WindowType::LISTBOX: + case WindowType::MULTILISTBOX: + case WindowType::TREELISTBOX: + aCtrlType = ControlType::Listbox; + break; + + case WindowType::COMBOBOX: + case WindowType::PATTERNBOX: + case WindowType::NUMERICBOX: + case WindowType::METRICBOX: + case WindowType::CURRENCYBOX: + case WindowType::DATEBOX: + case WindowType::TIMEBOX: + case WindowType::LONGCURRENCYBOX: + aCtrlType = ControlType::Combobox; + break; + default: + break; + } + + bool bDropDown = (IsSymbol() && (GetSymbol() == SymbolType::SPIN_DOWN) && GetText().isEmpty()); + + if( bDropDown && (aCtrlType == ControlType::Combobox || aCtrlType == ControlType::Listbox)) + { + if (GetParent()->IsNativeControlSupported(aCtrlType, ControlPart::Entire)) + { + // skip painting if the button was already drawn by the theme + if (aCtrlType == ControlType::Combobox) + { + Edit* pEdit = static_cast<Edit*>(GetParent()); + if (pEdit->ImplUseNativeBorder(rRenderContext, pEdit->GetStyle())) + bNativeOK = true; + } + else if (GetParent()->IsNativeControlSupported(aCtrlType, ControlPart::HasBackgroundTexture)) + { + bNativeOK = true; + } + + if (!bNativeOK && GetParent()->IsNativeControlSupported(aCtrlType, ControlPart::ButtonDown)) + { + // let the theme draw it, note we then need support + // for ControlType::Listbox/ControlPart::ButtonDown and ControlType::Combobox/ControlPart::ButtonDown + + ImplControlValue aControlValue; + ControlState nState = ControlState::NONE; + + if (mbPressed || mbIsActive) + nState |= ControlState::PRESSED; + if (GetButtonState() & DrawButtonFlags::Pressed) + nState |= ControlState::PRESSED; + if (HasFocus()) + nState |= ControlState::FOCUSED; + if (GetButtonState() & DrawButtonFlags::Default) + nState |= ControlState::DEFAULT; + if (Window::IsEnabled()) + nState |= ControlState::ENABLED; + + if (IsMouseOver() && aInRect.Contains(GetPointerPosPixel())) + nState |= ControlState::ROLLOVER; + + if ( IsMouseOver() && aInRect.Contains(GetPointerPosPixel()) && mbIsActive) + { + nState |= ControlState::ROLLOVER; + nButtonStyle &= ~DrawButtonFlags::Pressed; + } + + bNativeOK = rRenderContext.DrawNativeControl(aCtrlType, ControlPart::ButtonDown, aInRect, nState, + aControlValue, OUString()); + } + } + } + + if (bNativeOK) + return; + + bool bRollOver = (IsMouseOver() && aInRect.Contains(GetPointerPosPixel())); + if (bRollOver) + nButtonStyle |= DrawButtonFlags::Highlight; + bool bDrawMenuSep = mnDDStyle == PushButtonDropdownStyle::SplitMenuButton; + if (GetStyle() & WB_FLATBUTTON) + { + if (!bRollOver && !HasFocus()) + bDrawMenuSep = false; + } + // tdf#123175 if there is a custom control bg set, draw the button without outsourcing to the NWF + bNativeOK = !IsControlBackground() && rRenderContext.IsNativeControlSupported(ControlType::Pushbutton, ControlPart::Entire); + if (bNativeOK) + { + PushButtonValue aControlValue; + aControlValue.mbIsAction = isAction(); + + tools::Rectangle aCtrlRegion(aInRect); + ControlState nState = ControlState::NONE; + + if (mbPressed || IsChecked() || mbIsActive) + { + nState |= ControlState::PRESSED; + nButtonStyle |= DrawButtonFlags::Pressed; + } + if (GetButtonState() & DrawButtonFlags::Pressed) + nState |= ControlState::PRESSED; + if (HasFocus()) + nState |= ControlState::FOCUSED; + if (GetButtonState() & DrawButtonFlags::Default) + nState |= ControlState::DEFAULT; + if (Window::IsEnabled()) + nState |= ControlState::ENABLED; + + if (bRollOver || mbIsActive) + { + nButtonStyle |= DrawButtonFlags::Highlight; + nState |= ControlState::ROLLOVER; + } + + if (mbIsActive && bRollOver) + { + nState &= ~ControlState::PRESSED; + nButtonStyle &= ~DrawButtonFlags::Pressed; + } + + if (GetStyle() & WB_FLATBUTTON) + aControlValue.m_bFlatButton = true; + if (GetStyle() & WB_BEVELBUTTON) + aControlValue.mbBevelButton = true; + + // draw frame into invisible window to have aInRect modified correctly + // but do not shift the inner rect for pressed buttons (ie remove DrawButtonFlags::Pressed) + // this assumes the theme has enough visual cues to signalize the button was pressed + //Window aWin( this ); + //ImplDrawPushButtonFrame( &aWin, aInRect, nButtonStyle & ~DrawButtonFlags::Pressed ); + + // looks better this way as symbols were displaced slightly using the above approach + aInRect.AdjustTop(4 ); + aInRect.AdjustBottom( -4 ); + aInRect.AdjustLeft(4 ); + aInRect.AdjustRight( -4 ); + + // prepare single line hint (needed on mac to decide between normal push button and + // rectangular bevel button look) + Size aFontSize(Application::GetSettings().GetStyleSettings().GetPushButtonFont().GetFontSize()); + aFontSize = rRenderContext.LogicToPixel(aFontSize, MapMode(MapUnit::MapPoint)); + Size aInRectSize(rRenderContext.LogicToPixel(Size(aInRect.GetWidth(), aInRect.GetHeight()))); + aControlValue.mbSingleLine = (aInRectSize.Height() < 2 * aFontSize.Height()); + + if ((nState & ControlState::ROLLOVER) || !(GetStyle() & WB_FLATBUTTON) + || (HasFocus() && mpWindowImpl->mbUseNativeFocus + && !IsNativeControlSupported(ControlType::Pushbutton, ControlPart::Focus))) + { + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Pushbutton, ControlPart::Entire, aCtrlRegion, nState, + aControlValue, OUString() /*PushButton::GetText()*/); + } + else + { + bNativeOK = true; + } + + // draw content using the same aInRect as non-native VCL would do + ImplDrawPushButtonContent(&rRenderContext, SystemTextColorFlags::NONE, + aInRect, bDrawMenuSep, nButtonStyle); + + if (HasFocus()) + ShowFocus(ImplGetFocusRect()); + } + + if (bNativeOK) + return; + + // draw PushButtonFrame, aInRect has content size afterwards + if (GetStyle() & WB_FLATBUTTON) + { + tools::Rectangle aTempRect(aInRect); + if (bRollOver) + ImplDrawPushButtonFrame(rRenderContext, aTempRect, nButtonStyle); + aInRect.AdjustLeft(2 ); + aInRect.AdjustTop(2 ); + aInRect.AdjustRight( -2 ); + aInRect.AdjustBottom( -2 ); + } + else + { + ImplDrawPushButtonFrame(rRenderContext, aInRect, nButtonStyle); + } + + // draw content + ImplDrawPushButtonContent(&rRenderContext, SystemTextColorFlags::NONE, aInRect, bDrawMenuSep, nButtonStyle); + + if (HasFocus()) + { + ShowFocus(ImplGetFocusRect()); + } +} + +void PushButton::ImplSetDefButton( bool bSet ) +{ + Size aSize( GetSizePixel() ); + Point aPos( GetPosPixel() ); + int dLeft(0), dRight(0), dTop(0), dBottom(0); + bool bSetPos = false; + + if ( IsNativeControlSupported(ControlType::Pushbutton, ControlPart::Entire) ) + { + tools::Rectangle aBound, aCont; + tools::Rectangle aCtrlRegion( 0, 0, 80, 20 ); // use a constant size to avoid accumulating + // will not work if the theme has dynamic adornment sizes + ImplControlValue aControlValue; + + // get native size of a 'default' button + // and adjust the VCL button if more space for adornment is required + if( GetNativeControlRegion( ControlType::Pushbutton, ControlPart::Entire, aCtrlRegion, + ControlState::DEFAULT|ControlState::ENABLED, + aControlValue, + aBound, aCont ) ) + { + dLeft = aCont.Left() - aBound.Left(); + dTop = aCont.Top() - aBound.Top(); + dRight = aBound.Right() - aCont.Right(); + dBottom = aBound.Bottom() - aCont.Bottom(); + bSetPos = dLeft || dTop || dRight || dBottom; + } + } + + if ( bSet ) + { + if( !(GetButtonState() & DrawButtonFlags::Default) && bSetPos ) + { + // adjust pos/size when toggling from non-default to default + aPos.Move(-dLeft, -dTop); + aSize.AdjustWidth(dLeft + dRight ); + aSize.AdjustHeight(dTop + dBottom ); + } + GetButtonState() |= DrawButtonFlags::Default; + } + else + { + if( (GetButtonState() & DrawButtonFlags::Default) && bSetPos ) + { + // adjust pos/size when toggling from default to non-default + aPos.Move(dLeft, dTop); + aSize.AdjustWidth( -(dLeft + dRight) ); + aSize.AdjustHeight( -(dTop + dBottom) ); + } + GetButtonState() &= ~DrawButtonFlags::Default; + } + if( bSetPos ) + setPosSizePixel( aPos.X(), aPos.Y(), aSize.Width(), aSize.Height() ); + + Invalidate(); +} + +bool PushButton::ImplIsDefButton() const +{ + return bool(GetButtonState() & DrawButtonFlags::Default); +} + +PushButton::PushButton( WindowType nType ) : + Button( nType ) +{ + ImplInitPushButtonData(); +} + +PushButton::PushButton( vcl::Window* pParent, WinBits nStyle ) : + Button( WindowType::PUSHBUTTON ) +{ + ImplInitPushButtonData(); + ImplInit( pParent, nStyle ); +} + +void PushButton::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if ( !(rMEvt.IsLeft() && + ImplHitTestPushButton( this, rMEvt.GetPosPixel() )) ) + return; + + StartTrackingFlags nTrackFlags = StartTrackingFlags::NONE; + + if ( ( GetStyle() & WB_REPEAT ) && + ! ( GetStyle() & WB_TOGGLE ) ) + nTrackFlags |= StartTrackingFlags::ButtonRepeat; + + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + StartTracking( nTrackFlags ); + + if ( nTrackFlags & StartTrackingFlags::ButtonRepeat ) + Click(); +} + +void PushButton::Tracking( const TrackingEvent& rTEvt ) +{ + if ( rTEvt.IsTrackingEnded() ) + { + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + if ( !(GetStyle() & WB_NOPOINTERFOCUS) && !rTEvt.IsTrackingCanceled() ) + GrabFocus(); + + if ( GetStyle() & WB_TOGGLE ) + { + // Don't toggle, when aborted + if ( !rTEvt.IsTrackingCanceled() ) + { + if ( IsChecked() ) + { + Check( false ); + GetButtonState() &= ~DrawButtonFlags::Pressed; + } + else + Check(); + } + } + else + GetButtonState() &= ~DrawButtonFlags::Pressed; + + Invalidate(); + + // do not call Click handler if aborted + if ( !rTEvt.IsTrackingCanceled() ) + { + if ( ! ( GetStyle() & WB_REPEAT ) || ( GetStyle() & WB_TOGGLE ) ) + Click(); + } + } + } + else + { + if ( ImplHitTestPushButton( this, rTEvt.GetMouseEvent().GetPosPixel() ) ) + { + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + if ( rTEvt.IsTrackingRepeat() && (GetStyle() & WB_REPEAT) && + ! ( GetStyle() & WB_TOGGLE ) ) + Click(); + } + else + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + } + } + else + { + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + } + } +} + +void PushButton::KeyInput( const KeyEvent& rKEvt ) +{ + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + if ( !aKeyCode.GetModifier() && + ((aKeyCode.GetCode() == KEY_RETURN) || (aKeyCode.GetCode() == KEY_SPACE)) ) + { + if ( !(GetButtonState() & DrawButtonFlags::Pressed) ) + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + } + + if ( ( GetStyle() & WB_REPEAT ) && + ! ( GetStyle() & WB_TOGGLE ) ) + Click(); + } + else if ( (GetButtonState() & DrawButtonFlags::Pressed) && (aKeyCode.GetCode() == KEY_ESCAPE) ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + else + Button::KeyInput( rKEvt ); +} + +void PushButton::KeyUp( const KeyEvent& rKEvt ) +{ + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + if ( (GetButtonState() & DrawButtonFlags::Pressed) && + ((aKeyCode.GetCode() == KEY_RETURN) || (aKeyCode.GetCode() == KEY_SPACE)) ) + { + if ( GetStyle() & WB_TOGGLE ) + { + if ( IsChecked() ) + { + Check( false ); + GetButtonState() &= ~DrawButtonFlags::Pressed; + } + else + Check(); + + Toggle(); + } + else + GetButtonState() &= ~DrawButtonFlags::Pressed; + + Invalidate(); + + if ( !( GetStyle() & WB_REPEAT ) || ( GetStyle() & WB_TOGGLE ) ) + Click(); + } + else + Button::KeyUp( rKEvt ); +} + +void PushButton::FillLayoutData() const +{ + mxLayoutData.emplace(); + const_cast<PushButton*>(this)->Invalidate(); +} + +void PushButton::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) +{ + const Image& rCustomButtonImage = GetCustomButtonImage(); + if (!!rCustomButtonImage) + { + rRenderContext.DrawImage(Point(0, 0), rCustomButtonImage); + return; + } + ImplDrawPushButton(rRenderContext); +} + +void PushButton::Draw( OutputDevice* pDev, const Point& rPos, + SystemTextColorFlags nFlags ) +{ + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + tools::Rectangle aRect( aPos, aSize ); + vcl::Font aFont = GetDrawPixelFont( pDev ); + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + + std::optional<StyleSettings> oOrigDevStyleSettings; + + if ( nFlags & SystemTextColorFlags::Mono ) + { + pDev->SetTextColor( COL_BLACK ); + } + else + { + pDev->SetTextColor( GetTextColor() ); + // DecoView uses the FaceColor... + AllSettings aSettings = pDev->GetSettings(); + StyleSettings aStyleSettings = aSettings.GetStyleSettings(); + oOrigDevStyleSettings = aStyleSettings; + if ( IsControlBackground() ) + aStyleSettings.SetFaceColor( GetControlBackground() ); + else + aStyleSettings.SetFaceColor( GetSettings().GetStyleSettings().GetFaceColor() ); + aSettings.SetStyleSettings( aStyleSettings ); + pDev->OutputDevice::SetSettings( aSettings ); + } + pDev->SetTextFillColor(); + + DecorationView aDecoView( pDev ); + DrawButtonFlags nButtonStyle = DrawButtonFlags::NONE; + if ( nFlags & SystemTextColorFlags::Mono ) + nButtonStyle |= DrawButtonFlags::Mono; + if ( IsChecked() ) + nButtonStyle |= DrawButtonFlags::Checked; + aRect = aDecoView.DrawButton( aRect, nButtonStyle ); + + ImplDrawPushButtonContent( pDev, nFlags, aRect, true, nButtonStyle ); + + // restore original settings (which are not affected by Push/Pop) after + // finished drawing + if (oOrigDevStyleSettings) + { + AllSettings aSettings = pDev->GetSettings(); + aSettings.SetStyleSettings(*oOrigDevStyleSettings); + pDev->OutputDevice::SetSettings( aSettings ); + } + + pDev->Pop(); +} + +void PushButton::Resize() +{ + Control::Resize(); + Invalidate(); +} + +void PushButton::GetFocus() +{ + ShowFocus( ImplGetFocusRect() ); + SetInputContext( InputContext( GetFont() ) ); + Button::GetFocus(); +} + +void PushButton::LoseFocus() +{ + EndSelection(); + HideFocus(); + Button::LoseFocus(); +} + +void PushButton::StateChanged( StateChangedType nType ) +{ + Button::StateChanged( nType ); + + if ( (nType == StateChangedType::Enable) || + (nType == StateChangedType::Text) || + (nType == StateChangedType::Data) || + (nType == StateChangedType::State) || + (nType == StateChangedType::UpdateMode) ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetWindow( GetWindowType::Prev ), GetStyle() ) ); + + bool bIsDefButton = ( GetStyle() & WB_DEFBUTTON ) != 0; + bool bWasDefButton = ( GetPrevStyle() & WB_DEFBUTTON ) != 0; + if ( bIsDefButton != bWasDefButton ) + ImplSetDefButton( bIsDefButton ); + + if ( IsReallyVisible() && IsUpdateMode() ) + { + if ( (GetPrevStyle() & PUSHBUTTON_VIEW_STYLE) != + (GetStyle() & PUSHBUTTON_VIEW_STYLE) ) + Invalidate(); + } + } + else if ( (nType == StateChangedType::Zoom) || + (nType == StateChangedType::ControlFont) ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +void PushButton::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Button::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +bool PushButton::PreNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE ) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if( pMouseEvt && (pMouseEvt->IsEnterWindow() || pMouseEvt->IsLeaveWindow()) ) + { + // trigger redraw as mouse over state has changed + + // TODO: move this to Window class or make it a member !!! + ControlType aCtrlType = ControlType::Generic; + switch( GetParent()->GetType() ) + { + case WindowType::LISTBOX: + case WindowType::MULTILISTBOX: + case WindowType::TREELISTBOX: + aCtrlType = ControlType::Listbox; + break; + + case WindowType::COMBOBOX: + case WindowType::PATTERNBOX: + case WindowType::NUMERICBOX: + case WindowType::METRICBOX: + case WindowType::CURRENCYBOX: + case WindowType::DATEBOX: + case WindowType::TIMEBOX: + case WindowType::LONGCURRENCYBOX: + aCtrlType = ControlType::Combobox; + break; + default: + break; + } + + bool bDropDown = ( IsSymbol() && (GetSymbol()==SymbolType::SPIN_DOWN) && GetText().isEmpty() ); + + if( bDropDown && GetParent()->IsNativeControlSupported( aCtrlType, ControlPart::Entire) && + !GetParent()->IsNativeControlSupported( aCtrlType, ControlPart::ButtonDown) ) + { + vcl::Window *pBorder = GetParent()->GetWindow( GetWindowType::Border ); + if(aCtrlType == ControlType::Combobox) + { + // only paint the button part to avoid flickering of the combobox text + tools::Rectangle aClipRect( Point(), GetOutputSizePixel() ); + aClipRect.SetPos(pBorder->ScreenToOutputPixel(OutputToScreenPixel(aClipRect.TopLeft()))); + pBorder->Invalidate( aClipRect ); + } + else + { + pBorder->Invalidate( InvalidateFlags::NoErase ); + } + } + else if( (GetStyle() & WB_FLATBUTTON) || + IsNativeControlSupported(ControlType::Pushbutton, ControlPart::Entire) ) + { + Invalidate(); + } + } + } + + return Button::PreNotify(rNEvt); +} + +void PushButton::Toggle() +{ + ImplCallEventListenersAndHandler( VclEventId::PushbuttonToggle, nullptr ); +} + +void PushButton::SetSymbol( SymbolType eSymbol ) +{ + if ( meSymbol != eSymbol ) + { + meSymbol = eSymbol; + CompatStateChanged( StateChangedType::Data ); + } +} + +void PushButton::SetSymbolAlign( SymbolAlign eAlign ) +{ + ImplSetSymbolAlign( eAlign ); +} + +void PushButton::SetDropDown( PushButtonDropdownStyle nStyle ) +{ + if ( mnDDStyle != nStyle ) + { + mnDDStyle = nStyle; + CompatStateChanged( StateChangedType::Data ); + } +} + +void PushButton::SetState( TriState eState ) +{ + if ( meState == eState ) + return; + + meState = eState; + if ( meState == TRISTATE_FALSE ) + GetButtonState() &= ~DrawButtonFlags(DrawButtonFlags::Checked | DrawButtonFlags::DontKnow); + else if ( meState == TRISTATE_TRUE ) + { + GetButtonState() &= ~DrawButtonFlags::DontKnow; + GetButtonState() |= DrawButtonFlags::Checked; + } + else // TRISTATE_INDET + { + GetButtonState() &= ~DrawButtonFlags::Checked; + GetButtonState() |= DrawButtonFlags::DontKnow; + } + + CompatStateChanged( StateChangedType::State ); + Toggle(); +} + +void PushButton::statusChanged(const css::frame::FeatureStateEvent& rEvent) +{ + Button::statusChanged(rEvent); + if (rEvent.State.has<bool>()) + SetPressed(rEvent.State.get<bool>()); +} + +void PushButton::SetPressed( bool bPressed ) +{ + if ( mbPressed != bPressed ) + { + mbPressed = bPressed; + CompatStateChanged( StateChangedType::Data ); + } +} + +void PushButton::EndSelection() +{ + EndTracking( TrackingEventFlags::Cancel ); + if ( !isDisposed() && + GetButtonState() & DrawButtonFlags::Pressed ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + if ( !mbPressed ) + Invalidate(); + } +} + +Size PushButton::CalcMinimumSize() const +{ + Size aSize; + + if ( IsSymbol() ) + { + if ( IsSmallSymbol ()) + aSize = Size( 16, 12 ); + else + aSize = Size( 26, 24 ); + } + else if ( Button::HasImage() ) + aSize = GetModeImage().GetSizePixel(); + if( mnDDStyle == PushButtonDropdownStyle::MenuButton || + mnDDStyle == PushButtonDropdownStyle::SplitMenuButton ) + { + tools::Long nSymbolSize = GetTextHeight() / 2 + 1; + aSize.AdjustWidth(2*nSymbolSize ); + } + if (!PushButton::GetText().isEmpty()) + { + Size textSize = GetTextRect( tools::Rectangle( Point(), Size( 0x7fffffff, 0x7fffffff ) ), + PushButton::GetText(), ImplGetTextStyle( SystemTextColorFlags::NONE ) ).GetSize(); + + tools::Long nTextHeight = textSize.Height() * 1.15; + + ImageAlign eImageAlign = GetImageAlign(); + // tdf#142337 only considering the simple top/bottom/left/right possibilities + if (eImageAlign == ImageAlign::Top || eImageAlign == ImageAlign::Bottom) + { + aSize.AdjustHeight(nTextHeight); + aSize.setWidth(std::max(aSize.Width(), textSize.Width())); + } + else + { + aSize.AdjustWidth(textSize.Width()); + aSize.setHeight(std::max(aSize.Height(), nTextHeight)); + } + } + + // cf. ImplDrawPushButton ... + if( (GetStyle() & WB_SMALLSTYLE) == 0 ) + { + aSize.AdjustWidth(24 ); + aSize.AdjustHeight(12 ); + } + + return CalcWindowSize( aSize ); +} + +Size PushButton::GetOptimalSize() const +{ + return CalcMinimumSize(); +} + +bool PushButton::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "has-default") + { + WinBits nBits = GetStyle(); + nBits &= ~WB_DEFBUTTON; + if (toBool(rValue)) + nBits |= WB_DEFBUTTON; + SetStyle(nBits); + } + else + return Button::set_property(rKey, rValue); + return true; +} + +void PushButton::ShowFocus(const tools::Rectangle& rRect) +{ + if (IsNativeControlSupported(ControlType::Pushbutton, ControlPart::Focus)) + { + PushButtonValue aControlValue; + aControlValue.mbIsAction = isAction(); + tools::Rectangle aInRect(Point(), GetOutputSizePixel()); + GetOutDev()->DrawNativeControl(ControlType::Pushbutton, ControlPart::Focus, aInRect, + ControlState::FOCUSED, aControlValue, OUString()); + } + Button::ShowFocus(rRect); +} + +void OKButton::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + set_id("ok"); + PushButton::ImplInit( pParent, nStyle ); + + SetText( GetStandardText( StandardButtonType::OK ) ); +} + +OKButton::OKButton( vcl::Window* pParent, WinBits nStyle ) : + PushButton( WindowType::OKBUTTON ) +{ + ImplInit( pParent, nStyle ); +} + +void OKButton::Click() +{ + // close parent if no link set + if ( !GetClickHdl() ) + { + vcl::Window* pParent = getNonLayoutParent(this); + if ( pParent->IsSystemWindow() ) + { + if ( pParent->IsDialog() ) + { + VclPtr<Dialog> xParent( static_cast<Dialog*>(pParent) ); + if ( xParent->IsInExecute() ) + xParent->EndDialog( RET_OK ); + // prevent recursive calls + else if ( !xParent->IsInClose() ) + { + if ( pParent->GetStyle() & WB_CLOSEABLE ) + xParent->Close(); + } + } + else + { + if ( pParent->GetStyle() & WB_CLOSEABLE ) + static_cast<SystemWindow*>(pParent)->Close(); + } + } + } + else + { + PushButton::Click(); + } +} + +void CancelButton::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + set_id("cancel"); + PushButton::ImplInit( pParent, nStyle ); + + SetText( GetStandardText( StandardButtonType::Cancel ) ); +} + +CancelButton::CancelButton( vcl::Window* pParent, WinBits nStyle ) : + PushButton( WindowType::CANCELBUTTON ) +{ + ImplInit( pParent, nStyle ); +} + +void CancelButton::Click() +{ + // close parent if link not set + if ( !GetClickHdl() ) + { + vcl::Window* pParent = getNonLayoutParent(this); + if ( pParent->IsSystemWindow() ) + { + if ( pParent->IsDialog() ) + { + if ( static_cast<Dialog*>(pParent)->IsInExecute() ) + static_cast<Dialog*>(pParent)->EndDialog(); + // prevent recursive calls + else if ( !static_cast<Dialog*>(pParent)->IsInClose() ) + { + if ( pParent->GetStyle() & WB_CLOSEABLE ) + static_cast<Dialog*>(pParent)->Close(); + } + } + else + { + if ( pParent->GetStyle() & WB_CLOSEABLE ) + static_cast<SystemWindow*>(pParent)->Close(); + } + } + } + else + { + PushButton::Click(); + } +} + +CloseButton::CloseButton( vcl::Window* pParent ) + : CancelButton(pParent, 0) +{ + SetText( GetStandardText( StandardButtonType::Close ) ); +} + +void HelpButton::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + set_id("help"); + PushButton::ImplInit( pParent, nStyle | WB_NOPOINTERFOCUS ); + + SetText( GetStandardText( StandardButtonType::Help ) ); +} + +HelpButton::HelpButton( vcl::Window* pParent, WinBits nStyle ) : + PushButton( WindowType::HELPBUTTON ) +{ + ImplInit( pParent, nStyle ); +} + +void HelpButton::Click() +{ + // trigger help if no link set + if ( !GetClickHdl() ) + { + vcl::Window* pFocusWin = Application::GetFocusWindow(); + if ( !pFocusWin || comphelper::LibreOfficeKit::isActive() ) + pFocusWin = this; + + HelpEvent aEvt( pFocusWin->GetPointerPosPixel(), HelpEventMode::CONTEXT ); + pFocusWin->RequestHelp( aEvt ); + } + PushButton::Click(); +} + +void HelpButton::StateChanged( StateChangedType nStateChange ) +{ + // Hide when we have no help URL. + if (comphelper::LibreOfficeKit::isActive() && + officecfg::Office::Common::Help::HelpRootURL::get().isEmpty()) + Hide(); + else + PushButton::StateChanged(nStateChange); +} + +void RadioButton::ImplInitRadioButtonData() +{ + mbChecked = false; + mbRadioCheck = true; + mbStateChanged = false; +} + +void RadioButton::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle(getPreviousSibling(pParent), nStyle); + Button::ImplInit( pParent, nStyle, nullptr ); + + ImplInitSettings( true ); +} + +WinBits RadioButton::ImplInitStyle( const vcl::Window* pPrevWindow, WinBits nStyle ) const +{ + if ( !(nStyle & WB_NOGROUP) && + (!pPrevWindow || (pPrevWindow->GetType() != WindowType::RADIOBUTTON)) ) + nStyle |= WB_GROUP; + if ( !(nStyle & WB_NOTABSTOP) ) + { + if ( IsChecked() ) + nStyle |= WB_TABSTOP; + else + nStyle &= ~WB_TABSTOP; + } + + return nStyle; +} + +const vcl::Font& RadioButton::GetCanonicalFont( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetRadioCheckFont(); +} + +const Color& RadioButton::GetCanonicalTextColor( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetRadioCheckTextColor(); +} + +void RadioButton::ImplInitSettings( bool bBackground ) +{ + Button::ImplInitSettings(); + + if ( !bBackground ) + return; + + vcl::Window* pParent = GetParent(); + if ( !IsControlBackground() && + (pParent->IsChildTransparentModeEnabled() || IsNativeControlSupported( ControlType::Radiobutton, ControlPart::Entire ) ) ) + { + EnableChildTransparentMode(); + SetParentClipMode( ParentClipMode::NoClip ); + SetPaintTransparent( true ); + SetBackground(); + if( IsNativeControlSupported( ControlType::Radiobutton, ControlPart::Entire ) ) + mpWindowImpl->mbUseNativeFocus = ImplGetSVData()->maNWFData.mbNoFocusRects; + } + else + { + EnableChildTransparentMode( false ); + SetParentClipMode(); + SetPaintTransparent( false ); + + if ( IsControlBackground() ) + SetBackground( GetControlBackground() ); + else + SetBackground( pParent->GetBackground() ); + } +} + +void RadioButton::ImplDrawRadioButtonState(vcl::RenderContext& rRenderContext) +{ + bool bNativeOK = false; + + // no native drawing for image radio buttons + if (!maImage && rRenderContext.IsNativeControlSupported(ControlType::Radiobutton, ControlPart::Entire)) + { + ImplControlValue aControlValue( mbChecked ? ButtonValue::On : ButtonValue::Off ); + tools::Rectangle aCtrlRect(maStateRect.TopLeft(), maStateRect.GetSize()); + ControlState nState = ControlState::NONE; + + if (GetButtonState() & DrawButtonFlags::Pressed) + nState |= ControlState::PRESSED; + if (HasFocus()) + nState |= ControlState::FOCUSED; + if (GetButtonState() & DrawButtonFlags::Default) + nState |= ControlState::DEFAULT; + if (IsEnabled()) + nState |= ControlState::ENABLED; + + if (IsMouseOver() && maMouseRect.Contains(GetPointerPosPixel())) + nState |= ControlState::ROLLOVER; + + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Radiobutton, ControlPart::Entire, aCtrlRect, + nState, aControlValue, OUString()); + } + + if (bNativeOK) + return; + + if (!maImage) + { + DrawButtonFlags nStyle = GetButtonState(); + if (!IsEnabled()) + nStyle |= DrawButtonFlags::Disabled; + if (mbChecked) + nStyle |= DrawButtonFlags::Checked; + Image aImage = GetRadioImage(rRenderContext.GetSettings(), nStyle); + if (IsZoom()) + rRenderContext.DrawImage(maStateRect.TopLeft(), maStateRect.GetSize(), aImage); + else + rRenderContext.DrawImage(maStateRect.TopLeft(), aImage); + } + else + { + HideFocus(); + + DecorationView aDecoView(&rRenderContext); + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + tools::Rectangle aImageRect = maStateRect; + Size aImageSize = maImage.GetSizePixel(); + bool bEnabled = IsEnabled(); + + aImageSize.setWidth( CalcZoom(aImageSize.Width()) ); + aImageSize.setHeight( CalcZoom(aImageSize.Height()) ); + + aImageRect.AdjustLeft( 1 ); + aImageRect.AdjustTop( 1 ); + aImageRect.AdjustRight( -1 ); + aImageRect.AdjustBottom( -1 ); + + // display border and selection status + aImageRect = aDecoView.DrawFrame(aImageRect, DrawFrameStyle::DoubleIn); + if ((GetButtonState() & DrawButtonFlags::Pressed) || !bEnabled) + rRenderContext.SetFillColor( rStyleSettings.GetFaceColor()); + else + rRenderContext.SetFillColor(rStyleSettings.GetFieldColor()); + rRenderContext.SetLineColor(); + rRenderContext.DrawRect(aImageRect); + + // display image + DrawImageFlags nImageStyle = DrawImageFlags::NONE; + if (!bEnabled) + nImageStyle |= DrawImageFlags::Disable; + + Image* pImage = &maImage; + + Point aImagePos(aImageRect.TopLeft()); + aImagePos.AdjustX((aImageRect.GetWidth() - aImageSize.Width()) / 2 ); + aImagePos.AdjustY((aImageRect.GetHeight() - aImageSize.Height()) / 2 ); + if (IsZoom()) + rRenderContext.DrawImage(aImagePos, aImageSize, *pImage, nImageStyle); + else + rRenderContext.DrawImage(aImagePos, *pImage, nImageStyle); + + aImageRect.AdjustLeft( 1 ); + aImageRect.AdjustTop( 1 ); + aImageRect.AdjustRight( -1 ); + aImageRect.AdjustBottom( -1 ); + + ImplSetFocusRect(aImageRect); + + if (mbChecked) + { + rRenderContext.SetLineColor(rStyleSettings.GetHighlightColor()); + rRenderContext.SetFillColor(); + if ((aImageSize.Width() >= 20) || (aImageSize.Height() >= 20)) + { + aImageRect.AdjustLeft( 1 ); + aImageRect.AdjustTop( 1 ); + aImageRect.AdjustRight( -1 ); + aImageRect.AdjustBottom( -1 ); + } + rRenderContext.DrawRect(aImageRect); + aImageRect.AdjustLeft( 1 ); + aImageRect.AdjustTop( 1 ); + aImageRect.AdjustRight( -1 ); + aImageRect.AdjustBottom( -1 ); + rRenderContext.DrawRect(aImageRect); + } + + if (HasFocus()) + ShowFocus(ImplGetFocusRect()); + } +} + +// for drawing RadioButton or CheckButton that has Text and/or Image +void Button::ImplDrawRadioCheck(OutputDevice* pDev, WinBits nWinStyle, SystemTextColorFlags nSystemTextColorFlags, + const Point& rPos, const Size& rSize, + const Size& rImageSize, tools::Rectangle& rStateRect, + tools::Rectangle& rMouseRect) +{ + DrawTextFlags nTextStyle = Button::ImplGetTextStyle( nWinStyle, nSystemTextColorFlags ); + + const tools::Long nImageSep = GetDrawPixel( pDev, ImplGetImageToTextDistance() ); + Size aSize( rSize ); + Point aPos( rPos ); + aPos.AdjustX(rImageSize.Width() + nImageSep ); + + // tdf#141761 Old (convenience?) adjustment of width may lead to empty + // or negative(!) Size, that needs to be avoided. The coordinate context + // is pixel-oriented (all Paints of Controls are, historically), so + // the minimum width should be '1' Pixel. + // Hint: nImageSep is based on Zoom (using Window::CalcZoom) and + // MapModes (using Window::GetDrawPixel) - so potentially a wide range + // of unpredictable values is possible + const tools::Long nWidthAdjust(rImageSize.Width() + nImageSep); + aSize.setWidth(std::max(static_cast<tools::Long>(1), aSize.getWidth() - nWidthAdjust)); + + // if the text rect height is smaller than the height of the image + // then for single lines the default should be centered text + if( (nWinStyle & (WB_TOP|WB_VCENTER|WB_BOTTOM)) == 0 && + (rImageSize.Height() > rSize.Height() || ! (nWinStyle & WB_WORDBREAK) ) ) + { + nTextStyle &= ~DrawTextFlags(DrawTextFlags::Top|DrawTextFlags::Bottom); + nTextStyle |= DrawTextFlags::VCenter; + aSize.setHeight( rImageSize.Height() ); + } + + ImplDrawAlignedImage( pDev, aPos, aSize, 1, nTextStyle ); + + rMouseRect = tools::Rectangle( aPos, aSize ); + rMouseRect.SetLeft( rPos.X() ); + + rStateRect.SetLeft( rPos.X() ); + rStateRect.SetTop( rMouseRect.Top() ); + + if ( aSize.Height() > rImageSize.Height() ) + rStateRect.AdjustTop(( aSize.Height() - rImageSize.Height() ) / 2 ); + else + { + rStateRect.AdjustTop( -(( rImageSize.Height() - aSize.Height() ) / 2) ); + if( rStateRect.Top() < 0 ) + rStateRect.SetTop( 0 ); + } + + rStateRect.SetRight( rStateRect.Left()+rImageSize.Width()-1 ); + rStateRect.SetBottom( rStateRect.Top()+rImageSize.Height()-1 ); + + if ( rStateRect.Bottom() > rMouseRect.Bottom() ) + rMouseRect.SetBottom( rStateRect.Bottom() ); +} + +void RadioButton::ImplDraw( OutputDevice* pDev, SystemTextColorFlags nSystemTextColorFlags, + const Point& rPos, const Size& rSize, + const Size& rImageSize, tools::Rectangle& rStateRect, + tools::Rectangle& rMouseRect ) +{ + WinBits nWinStyle = GetStyle(); + OUString aText( GetText() ); + + pDev->Push( vcl::PushFlags::CLIPREGION ); + pDev->IntersectClipRegion( tools::Rectangle( rPos, rSize ) ); + + // no image radio button + if ( !maImage ) + { + if (!aText.isEmpty() || HasImage()) + { + Button::ImplDrawRadioCheck(pDev, nWinStyle, nSystemTextColorFlags, + rPos, rSize, rImageSize, + rStateRect, rMouseRect); + } + else + { + rStateRect.SetLeft( rPos.X() ); + if ( nWinStyle & WB_VCENTER ) + rStateRect.SetTop( rPos.Y()+((rSize.Height()-rImageSize.Height())/2) ); + else if ( nWinStyle & WB_BOTTOM ) + rStateRect.SetTop( rPos.Y()+rSize.Height()-rImageSize.Height() ); //-1; + else + rStateRect.SetTop( rPos.Y() ); + rStateRect.SetRight( rStateRect.Left()+rImageSize.Width()-1 ); + rStateRect.SetBottom( rStateRect.Top()+rImageSize.Height()-1 ); + rMouseRect = rStateRect; + + ImplSetFocusRect( rStateRect ); + } + } + else + { + bool bTopImage = (nWinStyle & WB_TOP) != 0; + Size aImageSize = maImage.GetSizePixel(); + tools::Rectangle aImageRect( rPos, rSize ); + tools::Long nTextHeight = pDev->GetTextHeight(); + tools::Long nTextWidth = pDev->GetCtrlTextWidth( aText ); + + // calculate position and sizes + if (!aText.isEmpty()) + { + Size aTmpSize( (aImageSize.Width()+8), (aImageSize.Height()+8) ); + if ( bTopImage ) + { + aImageRect.SetLeft( (rSize.Width()-aTmpSize.Width())/2 ); + aImageRect.SetTop( (rSize.Height()-(aTmpSize.Height()+nTextHeight+6))/2 ); + } + else + aImageRect.SetTop( (rSize.Height()-aTmpSize.Height())/2 ); + + aImageRect.SetRight( aImageRect.Left()+aTmpSize.Width() ); + aImageRect.SetBottom( aImageRect.Top()+aTmpSize.Height() ); + + // display text + Point aTxtPos = rPos; + if ( bTopImage ) + { + aTxtPos.AdjustX((rSize.Width()-nTextWidth)/2 ); + aTxtPos.AdjustY(aImageRect.Bottom()+6 ); + } + else + { + aTxtPos.AdjustX(aImageRect.Right()+8 ); + aTxtPos.AdjustY((rSize.Height()-nTextHeight)/2 ); + } + pDev->DrawCtrlText( aTxtPos, aText, 0, aText.getLength() ); + } + + rMouseRect = aImageRect; + rStateRect = aImageRect; + } + + pDev->Pop(); +} + +void RadioButton::ImplDrawRadioButton(vcl::RenderContext& rRenderContext) +{ + HideFocus(); + + Size aImageSize; + if (!maImage) + aImageSize = ImplGetRadioImageSize(); + else + aImageSize = maImage.GetSizePixel(); + + aImageSize.setWidth( CalcZoom(aImageSize.Width()) ); + aImageSize.setHeight( CalcZoom(aImageSize.Height()) ); + + // Draw control text + ImplDraw(&rRenderContext, SystemTextColorFlags::NONE, Point(), GetOutputSizePixel(), + aImageSize, maStateRect, maMouseRect); + + if (!maImage && HasFocus()) + ShowFocus(ImplGetFocusRect()); + + ImplDrawRadioButtonState(rRenderContext); +} + +void RadioButton::group(RadioButton &rOther) +{ + if (&rOther == this) + return; + + if (!m_xGroup) + { + m_xGroup = std::make_shared<std::vector<VclPtr<RadioButton> >>(); + m_xGroup->push_back(this); + } + + auto aFind = std::find(m_xGroup->begin(), m_xGroup->end(), VclPtr<RadioButton>(&rOther)); + if (aFind == m_xGroup->end()) + { + m_xGroup->push_back(&rOther); + + if (rOther.m_xGroup) + { + std::vector< VclPtr<RadioButton> > aOthers(rOther.GetRadioButtonGroup(false)); + //make all members of the group share the same button group + for (auto const& elem : aOthers) + { + aFind = std::find(m_xGroup->begin(), m_xGroup->end(), elem); + if (aFind == m_xGroup->end()) + m_xGroup->push_back(elem); + } + } + + //make all members of the group share the same button group + for (VclPtr<RadioButton> const & pButton : *m_xGroup) + { + pButton->m_xGroup = m_xGroup; + } + } + + //if this one is checked, uncheck all the others + if (mbChecked) + ImplUncheckAllOther(); +} + +std::vector< VclPtr<RadioButton> > RadioButton::GetRadioButtonGroup(bool bIncludeThis) const +{ + if (m_xGroup) + { + if (bIncludeThis) + return *m_xGroup; + std::vector< VclPtr<RadioButton> > aGroup; + for (VclPtr<RadioButton> const & pRadioButton : *m_xGroup) + { + if (pRadioButton == this) + continue; + aGroup.push_back(pRadioButton); + } + return aGroup; + } + + std::vector<VclPtr<RadioButton>> aGroup; + if (mbUsesExplicitGroup) + return aGroup; + + //old-school + + // go back to first in group; + vcl::Window* pFirst = const_cast<RadioButton*>(this); + while( ( pFirst->GetStyle() & WB_GROUP ) == 0 ) + { + vcl::Window* pWindow = pFirst->GetWindow( GetWindowType::Prev ); + if( pWindow ) + pFirst = pWindow; + else + break; + } + // insert radiobuttons up to next group + do + { + if( pFirst->GetType() == WindowType::RADIOBUTTON ) + { + if( pFirst != this || bIncludeThis ) + aGroup.emplace_back(static_cast<RadioButton*>(pFirst) ); + } + pFirst = pFirst->GetWindow( GetWindowType::Next ); + } while( pFirst && ( ( pFirst->GetStyle() & WB_GROUP ) == 0 ) ); + + return aGroup; +} + +void RadioButton::ImplUncheckAllOther() +{ + mpWindowImpl->mnStyle |= WB_TABSTOP; + + std::vector<VclPtr<RadioButton> > aGroup(GetRadioButtonGroup(false)); + // iterate over radio button group and checked buttons + for (VclPtr<RadioButton>& pWindow : aGroup) + { + if ( pWindow->IsChecked() ) + { + pWindow->SetState( false ); + if ( pWindow->isDisposed() ) + return; + } + + // not inside if clause to always remove wrongly set WB_TABSTOPS + pWindow->mpWindowImpl->mnStyle &= ~WB_TABSTOP; + } +} + +void RadioButton::ImplCallClick( bool bGrabFocus, GetFocusFlags nFocusFlags ) +{ + mbStateChanged = !mbChecked; + mbChecked = true; + mpWindowImpl->mnStyle |= WB_TABSTOP; + Invalidate(); + VclPtr<vcl::Window> xWindow = this; + if ( mbRadioCheck ) + ImplUncheckAllOther(); + if ( xWindow->isDisposed() ) + return; + if ( bGrabFocus ) + ImplGrabFocus( nFocusFlags ); + if ( xWindow->isDisposed() ) + return; + if ( mbStateChanged ) + Toggle(); + if ( xWindow->isDisposed() ) + return; + Click(); + if ( xWindow->isDisposed() ) + return; + mbStateChanged = false; +} + +RadioButton::RadioButton(vcl::Window* pParent, bool bUsesExplicitGroup, WinBits nStyle) + : Button(WindowType::RADIOBUTTON) + , mbUsesExplicitGroup(bUsesExplicitGroup) +{ + ImplInitRadioButtonData(); + ImplInit( pParent, nStyle ); +} + +RadioButton::~RadioButton() +{ + disposeOnce(); +} + +void RadioButton::dispose() +{ + if (m_xGroup) + { + m_xGroup->erase(std::remove(m_xGroup->begin(), m_xGroup->end(), VclPtr<RadioButton>(this)), + m_xGroup->end()); + m_xGroup.reset(); + } + Button::dispose(); +} + +void RadioButton::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if ( rMEvt.IsLeft() && maMouseRect.Contains( rMEvt.GetPosPixel() ) ) + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + StartTracking(); + return; + } + + Button::MouseButtonDown( rMEvt ); +} + +void RadioButton::Tracking( const TrackingEvent& rTEvt ) +{ + if ( rTEvt.IsTrackingEnded() ) + { + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + if ( !(GetStyle() & WB_NOPOINTERFOCUS) && !rTEvt.IsTrackingCanceled() ) + GrabFocus(); + + GetButtonState() &= ~DrawButtonFlags::Pressed; + + // do not call click handler if aborted + if ( !rTEvt.IsTrackingCanceled() ) + ImplCallClick(); + else + { + Invalidate(); + } + } + } + else + { + if ( maMouseRect.Contains( rTEvt.GetMouseEvent().GetPosPixel() ) ) + { + if ( !(GetButtonState() & DrawButtonFlags::Pressed) ) + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + } + } + else + { + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + } + } +} + +void RadioButton::KeyInput( const KeyEvent& rKEvt ) +{ + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + if ( !aKeyCode.GetModifier() && (aKeyCode.GetCode() == KEY_SPACE) ) + { + if ( !(GetButtonState() & DrawButtonFlags::Pressed) ) + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + } + } + else if ( (GetButtonState() & DrawButtonFlags::Pressed) && (aKeyCode.GetCode() == KEY_ESCAPE) ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + else + Button::KeyInput( rKEvt ); +} + +void RadioButton::KeyUp( const KeyEvent& rKEvt ) +{ + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + if ( (GetButtonState() & DrawButtonFlags::Pressed) && (aKeyCode.GetCode() == KEY_SPACE) ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + ImplCallClick(); + } + else + Button::KeyUp( rKEvt ); +} + +void RadioButton::FillLayoutData() const +{ + mxLayoutData.emplace(); + const_cast<RadioButton*>(this)->Invalidate(); +} + +void RadioButton::Paint( vcl::RenderContext& rRenderContext, const tools::Rectangle& ) +{ + ImplDrawRadioButton(rRenderContext); +} + +void RadioButton::Draw( OutputDevice* pDev, const Point& rPos, + SystemTextColorFlags nFlags ) +{ + if ( !maImage ) + { + MapMode aResMapMode( MapUnit::Map100thMM ); + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + Size aImageSize = pDev->LogicToPixel( Size( 300, 300 ), aResMapMode ); + Size aBrd1Size = pDev->LogicToPixel( Size( 20, 20 ), aResMapMode ); + Size aBrd2Size = pDev->LogicToPixel( Size( 60, 60 ), aResMapMode ); + vcl::Font aFont = GetDrawPixelFont( pDev ); + tools::Rectangle aStateRect; + tools::Rectangle aMouseRect; + + aImageSize.setWidth( CalcZoom( aImageSize.Width() ) ); + aImageSize.setHeight( CalcZoom( aImageSize.Height() ) ); + aBrd1Size.setWidth( CalcZoom( aBrd1Size.Width() ) ); + aBrd1Size.setHeight( CalcZoom( aBrd1Size.Height() ) ); + aBrd2Size.setWidth( CalcZoom( aBrd2Size.Width() ) ); + aBrd2Size.setHeight( CalcZoom( aBrd2Size.Height() ) ); + + if ( !aBrd1Size.Width() ) + aBrd1Size.setWidth( 1 ); + if ( !aBrd1Size.Height() ) + aBrd1Size.setHeight( 1 ); + if ( !aBrd2Size.Width() ) + aBrd2Size.setWidth( 1 ); + if ( !aBrd2Size.Height() ) + aBrd2Size.setHeight( 1 ); + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + if ( nFlags & SystemTextColorFlags::Mono ) + pDev->SetTextColor( COL_BLACK ); + else + pDev->SetTextColor( GetTextColor() ); + pDev->SetTextFillColor(); + + ImplDraw( pDev, nFlags, aPos, aSize, + aImageSize, aStateRect, aMouseRect ); + + Point aCenterPos = aStateRect.Center(); + tools::Long nRadX = aImageSize.Width()/2; + tools::Long nRadY = aImageSize.Height()/2; + + pDev->SetLineColor(); + pDev->SetFillColor( COL_BLACK ); + pDev->DrawPolygon( tools::Polygon( aCenterPos, nRadX, nRadY ) ); + nRadX -= aBrd1Size.Width(); + nRadY -= aBrd1Size.Height(); + pDev->SetFillColor( COL_WHITE ); + pDev->DrawPolygon( tools::Polygon( aCenterPos, nRadX, nRadY ) ); + if ( mbChecked ) + { + nRadX -= aBrd1Size.Width(); + nRadY -= aBrd1Size.Height(); + if ( !nRadX ) + nRadX = 1; + if ( !nRadY ) + nRadY = 1; + pDev->SetFillColor( COL_BLACK ); + pDev->DrawPolygon( tools::Polygon( aCenterPos, nRadX, nRadY ) ); + } + + pDev->Pop(); + } + else + { + OSL_FAIL( "RadioButton::Draw() - not implemented for RadioButton with Image" ); + } +} + +void RadioButton::Resize() +{ + Control::Resize(); + Invalidate(); +} + +void RadioButton::GetFocus() +{ + ShowFocus( ImplGetFocusRect() ); + SetInputContext( InputContext( GetFont() ) ); + Button::GetFocus(); +} + +void RadioButton::LoseFocus() +{ + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + + HideFocus(); + Button::LoseFocus(); +} + +void RadioButton::StateChanged( StateChangedType nType ) +{ + Button::StateChanged( nType ); + + if ( nType == StateChangedType::State ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate( maStateRect ); + } + else if ( (nType == StateChangedType::Enable) || + (nType == StateChangedType::Text) || + (nType == StateChangedType::Data) || + (nType == StateChangedType::UpdateMode) ) + { + if ( IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetWindow( GetWindowType::Prev ), GetStyle() ) ); + + if ( (GetPrevStyle() & RADIOBUTTON_VIEW_STYLE) != + (GetStyle() & RADIOBUTTON_VIEW_STYLE) ) + { + if ( IsUpdateMode() ) + Invalidate(); + } + } + else if ( (nType == StateChangedType::Zoom) || + (nType == StateChangedType::ControlFont) ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +void RadioButton::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Button::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +bool RadioButton::PreNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE ) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if( pMouseEvt && !pMouseEvt->GetButtons() && !pMouseEvt->IsSynthetic() && !pMouseEvt->IsModifierChanged() ) + { + // trigger redraw if mouse over state has changed + if( IsNativeControlSupported(ControlType::Radiobutton, ControlPart::Entire) ) + { + if (maMouseRect.Contains(GetPointerPosPixel()) != maMouseRect.Contains(GetLastPointerPosPixel()) || + pMouseEvt->IsLeaveWindow() || pMouseEvt->IsEnterWindow()) + { + Invalidate( maStateRect ); + } + } + } + } + + return Button::PreNotify(rNEvt); +} + +void RadioButton::Toggle() +{ + ImplCallEventListenersAndHandler( VclEventId::RadiobuttonToggle, [this] () { maToggleHdl.Call(*this); } ); +} + +void RadioButton::SetModeRadioImage( const Image& rImage ) +{ + if ( rImage != maImage ) + { + maImage = rImage; + CompatStateChanged( StateChangedType::Data ); + queue_resize(); + } +} + + +void RadioButton::SetState( bool bCheck ) +{ + // carry the TabStop flag along correctly + if ( bCheck ) + mpWindowImpl->mnStyle |= WB_TABSTOP; + else + mpWindowImpl->mnStyle &= ~WB_TABSTOP; + + if ( mbChecked != bCheck ) + { + mbChecked = bCheck; + CompatStateChanged( StateChangedType::State ); + Toggle(); + } +} + +bool RadioButton::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "active") + SetState(toBool(rValue)); + else if (rKey == "image-position") + { + WinBits nBits = GetStyle(); + if (rValue == "left") + { + nBits &= ~(WB_CENTER | WB_RIGHT); + nBits |= WB_LEFT; + } + else if (rValue == "right") + { + nBits &= ~(WB_CENTER | WB_LEFT); + nBits |= WB_RIGHT; + } + else if (rValue == "top") + { + nBits &= ~(WB_VCENTER | WB_BOTTOM); + nBits |= WB_TOP; + } + else if (rValue == "bottom") + { + nBits &= ~(WB_VCENTER | WB_TOP); + nBits |= WB_BOTTOM; + } + //It's rather mad to have to set these bits when there is the other + //image align. Looks like e.g. the radiobuttons etc weren't converted + //over to image align fully. + SetStyle(nBits); + //Deliberate to set the sane ImageAlign property + return Button::set_property(rKey, rValue); + } + else + return Button::set_property(rKey, rValue); + return true; +} + +void RadioButton::Check( bool bCheck ) +{ + // TabStop-Flag richtig mitfuehren + if ( bCheck ) + mpWindowImpl->mnStyle |= WB_TABSTOP; + else + mpWindowImpl->mnStyle &= ~WB_TABSTOP; + + if ( mbChecked == bCheck ) + return; + + mbChecked = bCheck; + VclPtr<vcl::Window> xWindow = this; + CompatStateChanged( StateChangedType::State ); + if ( xWindow->isDisposed() ) + return; + if ( bCheck && mbRadioCheck ) + ImplUncheckAllOther(); + if ( xWindow->isDisposed() ) + return; + Toggle(); +} + +tools::Long Button::ImplGetImageToTextDistance() const +{ + // 4 pixels, but take zoom into account, so the text doesn't "jump" relative to surrounding elements, + // which might have been aligned with the text of the check box + return CalcZoom( 4 ); +} + +Size RadioButton::ImplGetRadioImageSize() const +{ + Size aSize; + bool bDefaultSize = true; + if( IsNativeControlSupported( ControlType::Radiobutton, ControlPart::Entire ) ) + { + ImplControlValue aControlValue; + tools::Rectangle aCtrlRegion( Point( 0, 0 ), GetSizePixel() ); + tools::Rectangle aBoundingRgn, aContentRgn; + + // get native size of a radio button + if( GetNativeControlRegion( ControlType::Radiobutton, ControlPart::Entire, aCtrlRegion, + ControlState::DEFAULT|ControlState::ENABLED, + aControlValue, + aBoundingRgn, aContentRgn ) ) + { + aSize = aContentRgn.GetSize(); + bDefaultSize = false; + } + } + if( bDefaultSize ) + aSize = GetRadioImage( GetSettings(), DrawButtonFlags::NONE ).GetSizePixel(); + return aSize; +} + +static void LoadThemedImageList(const StyleSettings &rStyleSettings, + std::vector<Image>& rList, const std::vector<OUString> &rResources) +{ + Color aColorAry1[6]; + Color aColorAry2[6]; + aColorAry1[0] = Color( 0xC0, 0xC0, 0xC0 ); + aColorAry1[1] = Color( 0xFF, 0xFF, 0x00 ); + aColorAry1[2] = Color( 0xFF, 0xFF, 0xFF ); + aColorAry1[3] = Color( 0x80, 0x80, 0x80 ); + aColorAry1[4] = Color( 0x00, 0x00, 0x00 ); + aColorAry1[5] = Color( 0x00, 0xFF, 0x00 ); + aColorAry2[0] = rStyleSettings.GetFaceColor(); + aColorAry2[1] = rStyleSettings.GetWindowColor(); + aColorAry2[2] = rStyleSettings.GetLightColor(); + aColorAry2[3] = rStyleSettings.GetShadowColor(); + aColorAry2[4] = rStyleSettings.GetDarkShadowColor(); + aColorAry2[5] = rStyleSettings.GetWindowTextColor(); + + static_assert( sizeof(aColorAry1) == sizeof(aColorAry2), "aColorAry1 must match aColorAry2" ); + + for (const auto &a : rResources) + { + BitmapEx aBmpEx(a); + aBmpEx.Replace(aColorAry1, aColorAry2, SAL_N_ELEMENTS(aColorAry1)); + rList.emplace_back(aBmpEx); + } +} + +Image RadioButton::GetRadioImage( const AllSettings& rSettings, DrawButtonFlags nFlags ) +{ + ImplSVData* pSVData = ImplGetSVData(); + const StyleSettings& rStyleSettings = rSettings.GetStyleSettings(); + sal_uInt16 nStyle = 0; + + if ( rStyleSettings.GetOptions() & StyleSettingsOptions::Mono ) + nStyle = STYLE_RADIOBUTTON_MONO; + + if ( pSVData->maCtrlData.maRadioImgList.empty() || + (pSVData->maCtrlData.mnRadioStyle != nStyle) || + (pSVData->maCtrlData.mnLastRadioFColor != rStyleSettings.GetFaceColor()) || + (pSVData->maCtrlData.mnLastRadioWColor != rStyleSettings.GetWindowColor()) || + (pSVData->maCtrlData.mnLastRadioLColor != rStyleSettings.GetLightColor()) ) + { + pSVData->maCtrlData.maRadioImgList.clear(); + + pSVData->maCtrlData.mnLastRadioFColor = rStyleSettings.GetFaceColor(); + pSVData->maCtrlData.mnLastRadioWColor = rStyleSettings.GetWindowColor(); + pSVData->maCtrlData.mnLastRadioLColor = rStyleSettings.GetLightColor(); + + std::vector<OUString> aResources; + if (nStyle) + { + aResources.emplace_back(SV_RESID_BITMAP_RADIOMONO1); + aResources.emplace_back(SV_RESID_BITMAP_RADIOMONO2); + aResources.emplace_back(SV_RESID_BITMAP_RADIOMONO3); + aResources.emplace_back(SV_RESID_BITMAP_RADIOMONO4); + aResources.emplace_back(SV_RESID_BITMAP_RADIOMONO5); + aResources.emplace_back(SV_RESID_BITMAP_RADIOMONO6); + } + else + { + aResources.emplace_back(SV_RESID_BITMAP_RADIO1); + aResources.emplace_back(SV_RESID_BITMAP_RADIO2); + aResources.emplace_back(SV_RESID_BITMAP_RADIO3); + aResources.emplace_back(SV_RESID_BITMAP_RADIO4); + aResources.emplace_back(SV_RESID_BITMAP_RADIO5); + aResources.emplace_back(SV_RESID_BITMAP_RADIO6); + } + LoadThemedImageList( rStyleSettings, pSVData->maCtrlData.maRadioImgList, aResources); + pSVData->maCtrlData.mnRadioStyle = nStyle; + } + + sal_uInt16 nIndex; + if ( nFlags & DrawButtonFlags::Disabled ) + { + if ( nFlags & DrawButtonFlags::Checked ) + nIndex = 5; + else + nIndex = 4; + } + else if ( nFlags & DrawButtonFlags::Pressed ) + { + if ( nFlags & DrawButtonFlags::Checked ) + nIndex = 3; + else + nIndex = 2; + } + else + { + if ( nFlags & DrawButtonFlags::Checked ) + nIndex = 1; + else + nIndex = 0; + } + return pSVData->maCtrlData.maRadioImgList[nIndex]; +} + +void RadioButton::ImplAdjustNWFSizes() +{ + GetOutDev()->Push( vcl::PushFlags::MAPMODE ); + SetMapMode(MapMode(MapUnit::MapPixel)); + + ImplControlValue aControlValue; + Size aCurSize( GetSizePixel() ); + tools::Rectangle aCtrlRegion( Point( 0, 0 ), aCurSize ); + tools::Rectangle aBoundingRgn, aContentRgn; + + // get native size of a radiobutton + if( GetNativeControlRegion( ControlType::Radiobutton, ControlPart::Entire, aCtrlRegion, + ControlState::DEFAULT|ControlState::ENABLED, aControlValue, + aBoundingRgn, aContentRgn ) ) + { + Size aSize = aContentRgn.GetSize(); + + if( aSize.Height() > aCurSize.Height() ) + { + aCurSize.setHeight( aSize.Height() ); + SetSizePixel( aCurSize ); + } + } + + GetOutDev()->Pop(); +} + +Size RadioButton::CalcMinimumSize(tools::Long nMaxWidth) const +{ + Size aSize; + if ( !maImage ) + aSize = ImplGetRadioImageSize(); + else + { + aSize = maImage.GetSizePixel(); + aSize.AdjustWidth(8); + aSize.AdjustHeight(8); + } + + if (Button::HasImage()) + { + Size aImgSize = GetModeImage().GetSizePixel(); + aSize = Size(std::max(aImgSize.Width(), aSize.Width()), + std::max(aImgSize.Height(), aSize.Height())); + } + + OUString aText = GetText(); + if (!aText.isEmpty()) + { + bool bTopImage = (GetStyle() & WB_TOP) != 0; + + Size aTextSize = GetTextRect( tools::Rectangle( Point(), Size( nMaxWidth > 0 ? nMaxWidth : 0x7fffffff, 0x7fffffff ) ), + aText, FixedText::ImplGetTextStyle( GetStyle() ) ).GetSize(); + + aSize.AdjustWidth(2 ); // for focus rect + + if (!bTopImage) + { + aSize.AdjustWidth(ImplGetImageToTextDistance() ); + aSize.AdjustWidth(aTextSize.Width() ); + if ( aSize.Height() < aTextSize.Height() ) + aSize.setHeight( aTextSize.Height() ); + } + else + { + aSize.AdjustHeight(6 ); + aSize.AdjustHeight(GetTextHeight() ); + if ( aSize.Width() < aTextSize.Width() ) + aSize.setWidth( aTextSize.Width() ); + } + } + + return CalcWindowSize( aSize ); +} + +Size RadioButton::GetOptimalSize() const +{ + return CalcMinimumSize(); +} + +void RadioButton::ShowFocus(const tools::Rectangle& rRect) +{ + if (IsNativeControlSupported(ControlType::Radiobutton, ControlPart::Focus)) + { + ImplControlValue aControlValue; + tools::Rectangle aInRect(Point(0, 0), GetSizePixel()); + + aInRect.SetLeft( rRect.Left() ); // exclude the radio element itself from the focusrect + + GetOutDev()->DrawNativeControl(ControlType::Radiobutton, ControlPart::Focus, aInRect, + ControlState::FOCUSED, aControlValue, OUString()); + } + Button::ShowFocus(rRect); +} + +void RadioButton::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Button::DumpAsPropertyTree(rJsonWriter); + rJsonWriter.put("checked", IsChecked()); + + OUString sGroupId; + std::vector<VclPtr<RadioButton>> aGroup = GetRadioButtonGroup(); + for(const auto& pButton : aGroup) + sGroupId += pButton->get_id(); + + if (!sGroupId.isEmpty()) + rJsonWriter.put("group", sGroupId); + + if (!!maImage) + { + SvMemoryStream aOStm(6535, 6535); + if(GraphicConverter::Export(aOStm, maImage.GetBitmapEx(), ConvertDataFormat::PNG) == ERRCODE_NONE) + { + css::uno::Sequence<sal_Int8> aSeq( static_cast<sal_Int8 const *>(aOStm.GetData()), aOStm.Tell()); + OUStringBuffer aBuffer("data:image/png;base64,"); + ::comphelper::Base64::encode(aBuffer, aSeq); + rJsonWriter.put("image", aBuffer.makeStringAndClear()); + } + } +} + +FactoryFunction RadioButton::GetUITestFactory() const +{ + return RadioButtonUIObject::create; +} + +void CheckBox::ImplInitCheckBoxData() +{ + meState = TRISTATE_FALSE; + mbTriState = false; +} + +void CheckBox::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle(getPreviousSibling(pParent), nStyle); + Button::ImplInit( pParent, nStyle, nullptr ); + + ImplInitSettings( true ); +} + +WinBits CheckBox::ImplInitStyle( const vcl::Window* pPrevWindow, WinBits nStyle ) +{ + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + if ( !(nStyle & WB_NOGROUP) && + (!pPrevWindow || (pPrevWindow->GetType() != WindowType::CHECKBOX)) ) + nStyle |= WB_GROUP; + return nStyle; +} + +const vcl::Font& CheckBox::GetCanonicalFont( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetRadioCheckFont(); +} + +const Color& CheckBox::GetCanonicalTextColor( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetRadioCheckTextColor(); +} + +void CheckBox::ImplInitSettings( bool bBackground ) +{ + Button::ImplInitSettings(); + + if ( !bBackground ) + return; + + vcl::Window* pParent = GetParent(); + if ( !IsControlBackground() && + (pParent->IsChildTransparentModeEnabled() || IsNativeControlSupported( ControlType::Checkbox, ControlPart::Entire ) ) ) + { + EnableChildTransparentMode(); + SetParentClipMode( ParentClipMode::NoClip ); + SetPaintTransparent( true ); + SetBackground(); + if( IsNativeControlSupported( ControlType::Checkbox, ControlPart::Entire ) ) + ImplGetWindowImpl()->mbUseNativeFocus = ImplGetSVData()->maNWFData.mbNoFocusRects; + } + else + { + EnableChildTransparentMode( false ); + SetParentClipMode(); + SetPaintTransparent( false ); + + if ( IsControlBackground() ) + SetBackground( GetControlBackground() ); + else + SetBackground( pParent->GetBackground() ); + } +} + +void CheckBox::ImplDrawCheckBoxState(vcl::RenderContext& rRenderContext) +{ + bool bNativeOK = rRenderContext.IsNativeControlSupported(ControlType::Checkbox, ControlPart::Entire); + if (bNativeOK) + { + ImplControlValue aControlValue(meState == TRISTATE_TRUE ? ButtonValue::On : ButtonValue::Off); + tools::Rectangle aCtrlRegion(maStateRect); + ControlState nState = ControlState::NONE; + + if (HasFocus()) + nState |= ControlState::FOCUSED; + if (GetButtonState() & DrawButtonFlags::Default) + nState |= ControlState::DEFAULT; + if (GetButtonState() & DrawButtonFlags::Pressed) + nState |= ControlState::PRESSED; + if (IsEnabled()) + nState |= ControlState::ENABLED; + + if (meState == TRISTATE_TRUE) + aControlValue.setTristateVal(ButtonValue::On); + else if (meState == TRISTATE_INDET) + aControlValue.setTristateVal(ButtonValue::Mixed); + + if (IsMouseOver() && maMouseRect.Contains(GetPointerPosPixel())) + nState |= ControlState::ROLLOVER; + + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Checkbox, ControlPart::Entire, aCtrlRegion, + nState, aControlValue, OUString()); + } + + if (bNativeOK) + return; + + DrawButtonFlags nStyle = GetButtonState(); + if (!IsEnabled()) + nStyle |= DrawButtonFlags::Disabled; + if (meState == TRISTATE_INDET) + nStyle |= DrawButtonFlags::DontKnow; + else if (meState == TRISTATE_TRUE) + nStyle |= DrawButtonFlags::Checked; + Image aImage = GetCheckImage(GetSettings(), nStyle); + if (IsZoom()) + rRenderContext.DrawImage(maStateRect.TopLeft(), maStateRect.GetSize(), aImage); + else + rRenderContext.DrawImage(maStateRect.TopLeft(), aImage); +} + +void CheckBox::ImplDraw( OutputDevice* pDev, SystemTextColorFlags nSystemTextColorFlags, + const Point& rPos, const Size& rSize, + const Size& rImageSize, tools::Rectangle& rStateRect, + tools::Rectangle& rMouseRect ) +{ + WinBits nWinStyle = GetStyle(); + OUString aText( GetText() ); + + pDev->Push( vcl::PushFlags::CLIPREGION | vcl::PushFlags::LINECOLOR ); + pDev->IntersectClipRegion( tools::Rectangle( rPos, rSize ) ); + + if (!aText.isEmpty() || HasImage()) + { + Button::ImplDrawRadioCheck(pDev, nWinStyle, nSystemTextColorFlags, + rPos, rSize, rImageSize, + rStateRect, rMouseRect); + } + else + { + rStateRect.SetLeft( rPos.X() ); + if ( nWinStyle & WB_VCENTER ) + rStateRect.SetTop( rPos.Y()+((rSize.Height()-rImageSize.Height())/2) ); + else if ( nWinStyle & WB_BOTTOM ) + rStateRect.SetTop( rPos.Y()+rSize.Height()-rImageSize.Height() ); + else + rStateRect.SetTop( rPos.Y() ); + rStateRect.SetRight( rStateRect.Left()+rImageSize.Width()-1 ); + rStateRect.SetBottom( rStateRect.Top()+rImageSize.Height()-1 ); + // provide space for focusrect + // note: this assumes that the control's size was adjusted + // accordingly in Get/LoseFocus, so the onscreen position won't change + if( HasFocus() ) + rStateRect.Move( 1, 1 ); + rMouseRect = rStateRect; + + ImplSetFocusRect( rStateRect ); + } + + pDev->Pop(); +} + +void CheckBox::ImplDrawCheckBox(vcl::RenderContext& rRenderContext) +{ + Size aImageSize = ImplGetCheckImageSize(); + aImageSize.setWidth( CalcZoom( aImageSize.Width() ) ); + aImageSize.setHeight( CalcZoom( aImageSize.Height() ) ); + + HideFocus(); + + ImplDraw(&rRenderContext, SystemTextColorFlags::NONE, Point(), GetOutputSizePixel(), + aImageSize, maStateRect, maMouseRect); + + ImplDrawCheckBoxState(rRenderContext); + if (HasFocus()) + ShowFocus(ImplGetFocusRect()); +} + +void CheckBox::ImplCheck() +{ + TriState eNewState; + if ( meState == TRISTATE_FALSE ) + eNewState = TRISTATE_TRUE; + else if ( !mbTriState ) + eNewState = TRISTATE_FALSE; + else if ( meState == TRISTATE_TRUE ) + eNewState = TRISTATE_INDET; + else + eNewState = TRISTATE_FALSE; + meState = eNewState; + + VclPtr<vcl::Window> xWindow = this; + Invalidate(); + Toggle(); + if ( xWindow->isDisposed() ) + return; + Click(); +} + +CheckBox::CheckBox( vcl::Window* pParent, WinBits nStyle ) : + Button( WindowType::CHECKBOX ) +{ + ImplInitCheckBoxData(); + ImplInit( pParent, nStyle ); +} + +void CheckBox::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if ( rMEvt.IsLeft() && maMouseRect.Contains( rMEvt.GetPosPixel() ) ) + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + StartTracking(); + return; + } + + Button::MouseButtonDown( rMEvt ); +} + +void CheckBox::Tracking( const TrackingEvent& rTEvt ) +{ + if ( rTEvt.IsTrackingEnded() ) + { + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + if ( !(GetStyle() & WB_NOPOINTERFOCUS) && !rTEvt.IsTrackingCanceled() ) + GrabFocus(); + + GetButtonState() &= ~DrawButtonFlags::Pressed; + + // do not call click handler if aborted + if ( !rTEvt.IsTrackingCanceled() ) + ImplCheck(); + else + { + Invalidate(); + } + } + } + else + { + if ( maMouseRect.Contains( rTEvt.GetMouseEvent().GetPosPixel() ) ) + { + if ( !(GetButtonState() & DrawButtonFlags::Pressed) ) + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + } + } + else + { + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + } + } +} + +void CheckBox::KeyInput( const KeyEvent& rKEvt ) +{ + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + if ( !aKeyCode.GetModifier() && (aKeyCode.GetCode() == KEY_SPACE) ) + { + if ( !(GetButtonState() & DrawButtonFlags::Pressed) ) + { + GetButtonState() |= DrawButtonFlags::Pressed; + Invalidate(); + } + } + else if ( (GetButtonState() & DrawButtonFlags::Pressed) && (aKeyCode.GetCode() == KEY_ESCAPE) ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + else + Button::KeyInput( rKEvt ); +} + +void CheckBox::KeyUp( const KeyEvent& rKEvt ) +{ + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + if ( (GetButtonState() & DrawButtonFlags::Pressed) && (aKeyCode.GetCode() == KEY_SPACE) ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + ImplCheck(); + } + else + Button::KeyUp( rKEvt ); +} + +void CheckBox::FillLayoutData() const +{ + mxLayoutData.emplace(); + const_cast<CheckBox*>(this)->Invalidate(); +} + +void CheckBox::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) +{ + ImplDrawCheckBox(rRenderContext); +} + +void CheckBox::Draw( OutputDevice* pDev, const Point& rPos, + SystemTextColorFlags nFlags ) +{ + MapMode aResMapMode( MapUnit::Map100thMM ); + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + Size aImageSize = pDev->LogicToPixel( Size( 300, 300 ), aResMapMode ); + Size aBrd1Size = pDev->LogicToPixel( Size( 20, 20 ), aResMapMode ); + Size aBrd2Size = pDev->LogicToPixel( Size( 30, 30 ), aResMapMode ); + tools::Long nCheckWidth = pDev->LogicToPixel( Size( 20, 20 ), aResMapMode ).Width(); + vcl::Font aFont = GetDrawPixelFont( pDev ); + tools::Rectangle aStateRect; + tools::Rectangle aMouseRect; + + aImageSize.setWidth( CalcZoom( aImageSize.Width() ) ); + aImageSize.setHeight( CalcZoom( aImageSize.Height() ) ); + aBrd1Size.setWidth( CalcZoom( aBrd1Size.Width() ) ); + aBrd1Size.setHeight( CalcZoom( aBrd1Size.Height() ) ); + aBrd2Size.setWidth( CalcZoom( aBrd2Size.Width() ) ); + aBrd2Size.setHeight( CalcZoom( aBrd2Size.Height() ) ); + + if ( !aBrd1Size.Width() ) + aBrd1Size.setWidth( 1 ); + if ( !aBrd1Size.Height() ) + aBrd1Size.setHeight( 1 ); + if ( !aBrd2Size.Width() ) + aBrd2Size.setWidth( 1 ); + if ( !aBrd2Size.Height() ) + aBrd2Size.setHeight( 1 ); + if ( !nCheckWidth ) + nCheckWidth = 1; + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + if ( nFlags & SystemTextColorFlags::Mono ) + pDev->SetTextColor( COL_BLACK ); + else + pDev->SetTextColor( GetTextColor() ); + pDev->SetTextFillColor(); + + ImplDraw( pDev, nFlags, aPos, aSize, + aImageSize, aStateRect, aMouseRect ); + + pDev->SetLineColor(); + pDev->SetFillColor( COL_BLACK ); + pDev->DrawRect( aStateRect ); + aStateRect.AdjustLeft(aBrd1Size.Width() ); + aStateRect.AdjustTop(aBrd1Size.Height() ); + aStateRect.AdjustRight( -(aBrd1Size.Width()) ); + aStateRect.AdjustBottom( -(aBrd1Size.Height()) ); + if ( meState == TRISTATE_INDET ) + pDev->SetFillColor( COL_LIGHTGRAY ); + else + pDev->SetFillColor( COL_WHITE ); + pDev->DrawRect( aStateRect ); + + if ( meState == TRISTATE_TRUE ) + { + aStateRect.AdjustLeft(aBrd2Size.Width() ); + aStateRect.AdjustTop(aBrd2Size.Height() ); + aStateRect.AdjustRight( -(aBrd2Size.Width()) ); + aStateRect.AdjustBottom( -(aBrd2Size.Height()) ); + Point aPos11( aStateRect.TopLeft() ); + Point aPos12( aStateRect.BottomRight() ); + Point aPos21( aStateRect.TopRight() ); + Point aPos22( aStateRect.BottomLeft() ); + Point aTempPos11( aPos11 ); + Point aTempPos12( aPos12 ); + Point aTempPos21( aPos21 ); + Point aTempPos22( aPos22 ); + pDev->SetLineColor( COL_BLACK ); + tools::Long nDX = 0; + for ( tools::Long i = 0; i < nCheckWidth; i++ ) + { + if ( !(i % 2) ) + { + aTempPos11.setX( aPos11.X()+nDX ); + aTempPos12.setX( aPos12.X()+nDX ); + aTempPos21.setX( aPos21.X()+nDX ); + aTempPos22.setX( aPos22.X()+nDX ); + } + else + { + nDX++; + aTempPos11.setX( aPos11.X()-nDX ); + aTempPos12.setX( aPos12.X()-nDX ); + aTempPos21.setX( aPos21.X()-nDX ); + aTempPos22.setX( aPos22.X()-nDX ); + } + pDev->DrawLine( aTempPos11, aTempPos12 ); + pDev->DrawLine( aTempPos21, aTempPos22 ); + } + } + + pDev->Pop(); +} + +void CheckBox::Resize() +{ + Control::Resize(); + Invalidate(); +} + +void CheckBox::GetFocus() +{ + if (GetText().isEmpty()) + { + // increase button size to have space for focus rect + // checkboxes without text will draw focusrect around the check + // See CheckBox::ImplDraw() + Point aPos( GetPosPixel() ); + Size aSize( GetSizePixel() ); + aPos.Move(-1,-1); + aSize.AdjustHeight(2 ); + aSize.AdjustWidth(2 ); + setPosSizePixel( aPos.X(), aPos.Y(), aSize.Width(), aSize.Height() ); + Invalidate(); + // Trigger drawing to initialize the mouse rectangle, otherwise the mouse button down + // handler would ignore the mouse event. + PaintImmediately(); + } + else + ShowFocus( ImplGetFocusRect() ); + + SetInputContext( InputContext( GetFont() ) ); + Button::GetFocus(); +} + +void CheckBox::LoseFocus() +{ + if ( GetButtonState() & DrawButtonFlags::Pressed ) + { + GetButtonState() &= ~DrawButtonFlags::Pressed; + Invalidate(); + } + + HideFocus(); + Button::LoseFocus(); + + if (GetText().isEmpty()) + { + // decrease button size again (see GetFocus()) + // checkboxes without text will draw focusrect around the check + Point aPos( GetPosPixel() ); + Size aSize( GetSizePixel() ); + aPos.Move(1,1); + aSize.AdjustHeight( -2 ); + aSize.AdjustWidth( -2 ); + setPosSizePixel( aPos.X(), aPos.Y(), aSize.Width(), aSize.Height() ); + Invalidate(); + } +} + +void CheckBox::StateChanged( StateChangedType nType ) +{ + Button::StateChanged( nType ); + + if ( nType == StateChangedType::State ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate( maStateRect ); + } + else if ( (nType == StateChangedType::Enable) || + (nType == StateChangedType::Text) || + (nType == StateChangedType::Data) || + (nType == StateChangedType::UpdateMode) ) + { + if ( IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetWindow( GetWindowType::Prev ), GetStyle() ) ); + + if ( (GetPrevStyle() & CHECKBOX_VIEW_STYLE) != + (GetStyle() & CHECKBOX_VIEW_STYLE) ) + { + if ( IsUpdateMode() ) + Invalidate(); + } + } + else if ( (nType == StateChangedType::Zoom) || + (nType == StateChangedType::ControlFont) ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +void CheckBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Button::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +bool CheckBox::PreNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE ) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if( pMouseEvt && !pMouseEvt->GetButtons() && !pMouseEvt->IsSynthetic() && !pMouseEvt->IsModifierChanged() ) + { + // trigger redraw if mouse over state has changed + if( IsNativeControlSupported(ControlType::Checkbox, ControlPart::Entire) ) + { + if (maMouseRect.Contains(GetPointerPosPixel()) != maMouseRect.Contains(GetLastPointerPosPixel()) || + pMouseEvt->IsLeaveWindow() || pMouseEvt->IsEnterWindow()) + { + Invalidate( maStateRect ); + } + } + } + } + + return Button::PreNotify(rNEvt); +} + +void CheckBox::Toggle() +{ + ImplCallEventListenersAndHandler( VclEventId::CheckboxToggle, [this] () { maToggleHdl.Call(*this); } ); +} + +void CheckBox::SetState( TriState eState ) +{ + if ( !mbTriState && (eState == TRISTATE_INDET) ) + eState = TRISTATE_FALSE; + + if ( meState != eState ) + { + meState = eState; + StateChanged( StateChangedType::State ); + Toggle(); + } +} + +bool CheckBox::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "active") + SetState(toBool(rValue) ? TRISTATE_TRUE : TRISTATE_FALSE); + else + return Button::set_property(rKey, rValue); + return true; +} + +void CheckBox::EnableTriState( bool bTriState ) +{ + if ( mbTriState != bTriState ) + { + mbTriState = bTriState; + + if ( !bTriState && (meState == TRISTATE_INDET) ) + SetState( TRISTATE_FALSE ); + } +} + +Size CheckBox::ImplGetCheckImageSize() const +{ + Size aSize; + bool bDefaultSize = true; + if( IsNativeControlSupported( ControlType::Checkbox, ControlPart::Entire ) ) + { + ImplControlValue aControlValue; + tools::Rectangle aCtrlRegion( Point( 0, 0 ), GetSizePixel() ); + tools::Rectangle aBoundingRgn, aContentRgn; + + // get native size of a check box + if( GetNativeControlRegion( ControlType::Checkbox, ControlPart::Entire, aCtrlRegion, + ControlState::DEFAULT|ControlState::ENABLED, + aControlValue, + aBoundingRgn, aContentRgn ) ) + { + aSize = aContentRgn.GetSize(); + bDefaultSize = false; + } + } + if( bDefaultSize ) + aSize = GetCheckImage( GetSettings(), DrawButtonFlags::NONE ).GetSizePixel(); + return aSize; +} + +Image CheckBox::GetCheckImage( const AllSettings& rSettings, DrawButtonFlags nFlags ) +{ + ImplSVData* pSVData = ImplGetSVData(); + const StyleSettings& rStyleSettings = rSettings.GetStyleSettings(); + sal_uInt16 nStyle = 0; + + if ( rStyleSettings.GetOptions() & StyleSettingsOptions::Mono ) + nStyle = STYLE_CHECKBOX_MONO; + + if ( pSVData->maCtrlData.maCheckImgList.empty() || + (pSVData->maCtrlData.mnCheckStyle != nStyle) || + (pSVData->maCtrlData.mnLastCheckFColor != rStyleSettings.GetFaceColor()) || + (pSVData->maCtrlData.mnLastCheckWColor != rStyleSettings.GetWindowColor()) || + (pSVData->maCtrlData.mnLastCheckLColor != rStyleSettings.GetLightColor()) ) + { + pSVData->maCtrlData.maCheckImgList.clear(); + + pSVData->maCtrlData.mnLastCheckFColor = rStyleSettings.GetFaceColor(); + pSVData->maCtrlData.mnLastCheckWColor = rStyleSettings.GetWindowColor(); + pSVData->maCtrlData.mnLastCheckLColor = rStyleSettings.GetLightColor(); + + std::vector<OUString> aResources; + if (nStyle) + { + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO1); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO2); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO3); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO4); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO5); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO6); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO7); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO8); + aResources.emplace_back(SV_RESID_BITMAP_CHECKMONO9); + } + else + { + aResources.emplace_back(SV_RESID_BITMAP_CHECK1); + aResources.emplace_back(SV_RESID_BITMAP_CHECK2); + aResources.emplace_back(SV_RESID_BITMAP_CHECK3); + aResources.emplace_back(SV_RESID_BITMAP_CHECK4); + aResources.emplace_back(SV_RESID_BITMAP_CHECK5); + aResources.emplace_back(SV_RESID_BITMAP_CHECK6); + aResources.emplace_back(SV_RESID_BITMAP_CHECK7); + aResources.emplace_back(SV_RESID_BITMAP_CHECK8); + aResources.emplace_back(SV_RESID_BITMAP_CHECK9); + } + LoadThemedImageList(rStyleSettings, pSVData->maCtrlData.maCheckImgList, aResources); + pSVData->maCtrlData.mnCheckStyle = nStyle; + } + + sal_uInt16 nIndex; + if ( nFlags & DrawButtonFlags::Disabled ) + { + if ( nFlags & DrawButtonFlags::DontKnow ) + nIndex = 8; + else if ( nFlags & DrawButtonFlags::Checked ) + nIndex = 5; + else + nIndex = 4; + } + else if ( nFlags & DrawButtonFlags::Pressed ) + { + if ( nFlags & DrawButtonFlags::DontKnow ) + nIndex = 7; + else if ( nFlags & DrawButtonFlags::Checked ) + nIndex = 3; + else + nIndex = 2; + } + else + { + if ( nFlags & DrawButtonFlags::DontKnow ) + nIndex = 6; + else if ( nFlags & DrawButtonFlags::Checked ) + nIndex = 1; + else + nIndex = 0; + } + return pSVData->maCtrlData.maCheckImgList[nIndex]; +} + +void CheckBox::ImplAdjustNWFSizes() +{ + GetOutDev()->Push( vcl::PushFlags::MAPMODE ); + SetMapMode(MapMode(MapUnit::MapPixel)); + + ImplControlValue aControlValue; + Size aCurSize( GetSizePixel() ); + tools::Rectangle aCtrlRegion( Point( 0, 0 ), aCurSize ); + tools::Rectangle aBoundingRgn, aContentRgn; + + // get native size of a radiobutton + if( GetNativeControlRegion( ControlType::Checkbox, ControlPart::Entire, aCtrlRegion, + ControlState::DEFAULT|ControlState::ENABLED, aControlValue, + aBoundingRgn, aContentRgn ) ) + { + Size aSize = aContentRgn.GetSize(); + + if( aSize.Height() > aCurSize.Height() ) + { + aCurSize.setHeight( aSize.Height() ); + SetSizePixel( aCurSize ); + } + } + + GetOutDev()->Pop(); +} + +Size CheckBox::CalcMinimumSize( tools::Long nMaxWidth ) const +{ + Size aSize = ImplGetCheckImageSize(); + nMaxWidth -= aSize.Width(); + + OUString aText = GetText(); + if (!aText.isEmpty()) + { + // subtract what will be added later + nMaxWidth-=2; + nMaxWidth -= ImplGetImageToTextDistance(); + + Size aTextSize = GetTextRect( tools::Rectangle( Point(), Size( nMaxWidth > 0 ? nMaxWidth : 0x7fffffff, 0x7fffffff ) ), + aText, FixedText::ImplGetTextStyle( GetStyle() ) ).GetSize(); + aSize.AdjustWidth(2 ); // for focus rect + aSize.AdjustWidth(ImplGetImageToTextDistance() ); + aSize.AdjustWidth(aTextSize.Width() ); + if ( aSize.Height() < aTextSize.Height() ) + aSize.setHeight( aTextSize.Height() ); + } + else + { + // is this still correct ? since the checkbox now + // shows a focus rect it should be 2 pixels wider and longer +/* since otherwise the controls in the Writer hang too far up + aSize.Width() += 2; + aSize.Height() += 2; +*/ + } + + return CalcWindowSize( aSize ); +} + +Size CheckBox::GetOptimalSize() const +{ + int nWidthRequest(get_width_request()); + return CalcMinimumSize(nWidthRequest != -1 ? nWidthRequest : 0); +} + +void CheckBox::ShowFocus(const tools::Rectangle& rRect) +{ + if (IsNativeControlSupported(ControlType::Checkbox, ControlPart::Focus)) + { + ImplControlValue aControlValue; + tools::Rectangle aInRect(Point(0, 0), GetSizePixel()); + + aInRect.SetLeft( rRect.Left() ); // exclude the checkbox itself from the focusrect + + GetOutDev()->DrawNativeControl(ControlType::Checkbox, ControlPart::Focus, aInRect, + ControlState::FOCUSED, aControlValue, OUString()); + } + Button::ShowFocus(rRect); +} + +void CheckBox::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Button::DumpAsPropertyTree(rJsonWriter); + rJsonWriter.put("checked", IsChecked()); +} + +FactoryFunction CheckBox::GetUITestFactory() const +{ + return CheckBoxUIObject::create; +} + +ImageButton::ImageButton( vcl::Window* pParent, WinBits nStyle ) : + PushButton( pParent, nStyle ) +{ + ImplInitStyle(); +} + +void ImageButton::ImplInitStyle() +{ + WinBits nStyle = GetStyle(); + + if ( ! ( nStyle & ( WB_RIGHT | WB_LEFT ) ) ) + nStyle |= WB_CENTER; + + if ( ! ( nStyle & ( WB_TOP | WB_BOTTOM ) ) ) + nStyle |= WB_VCENTER; + + SetStyle( nStyle ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/calendar.cxx b/vcl/source/control/calendar.cxx new file mode 100644 index 000000000..7554d0d99 --- /dev/null +++ b/vcl/source/control/calendar.cxx @@ -0,0 +1,1723 @@ +/* -*- 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 <vcl/builder.hxx> +#include <vcl/svapp.hxx> +#include <vcl/help.hxx> +#include <vcl/menu.hxx> +#include <vcl/settings.hxx> +#include <vcl/event.hxx> +#include <vcl/toolkit/calendar.hxx> +#include <vcl/commandevent.hxx> +#include <vcl/dockwin.hxx> +#include <unotools/calendarwrapper.hxx> +#include <unotools/localedatawrapper.hxx> +#include <com/sun/star/i18n/Weekdays.hpp> +#include <com/sun/star/i18n/CalendarDisplayIndex.hpp> +#include <com/sun/star/i18n/CalendarFieldIndex.hpp> +#include <sal/log.hxx> + +#include <calendar.hxx> +#include <svdata.hxx> +#include <strings.hrc> +#include <memory> + +#define DAY_OFFX 4 +#define DAY_OFFY 2 +#define MONTH_BORDERX 4 +#define MONTH_OFFY 3 +#define WEEKDAY_OFFY 3 +#define TITLE_OFFY 3 +#define TITLE_BORDERY 2 +#define SPIN_OFFX 4 +#define SPIN_OFFY TITLE_BORDERY + +#define CALENDAR_HITTEST_DAY (sal_uInt16(0x0001)) +#define CALENDAR_HITTEST_MONTHTITLE (sal_uInt16(0x0004)) +#define CALENDAR_HITTEST_PREV (sal_uInt16(0x0008)) +#define CALENDAR_HITTEST_NEXT (sal_uInt16(0x0010)) + +#define MENU_YEAR_COUNT 3 + +using namespace ::com::sun::star; + +static void ImplCalendarSelectDate( IntDateSet* pTable, const Date& rDate, bool bSelect ) +{ + if ( bSelect ) + pTable->insert( rDate.GetDate() ); + else + pTable->erase( rDate.GetDate() ); +} + + + +void Calendar::ImplInit( WinBits nWinStyle ) +{ + mpSelectTable.reset(new IntDateSet); + mnDayCount = 0; + mnWinStyle = nWinStyle; + mnFirstYear = 0; + mnLastYear = 0; + mbCalc = true; + mbFormat = true; + mbDrag = false; + mbMenuDown = false; + mbSpinDown = false; + mbPrevIn = false; + mbNextIn = false; + + OUString aGregorian( "gregorian"); + maCalendarWrapper.loadCalendar( aGregorian, + Application::GetAppLocaleDataWrapper().getLanguageTag().getLocale()); + if (maCalendarWrapper.getUniqueID() != aGregorian) + { + SAL_WARN( "vcl.control", "Calendar::ImplInit: No ``gregorian'' calendar available for locale ``" + << Application::GetAppLocaleDataWrapper().getLanguageTag().getBcp47() + << "'' and other calendars aren't supported. Using en-US fallback." ); + + /* If we ever wanted to support other calendars than Gregorian a lot of + * rewrite would be necessary to internally replace use of class Date + * with proper class CalendarWrapper methods, get rid of fixed 12 + * months, fixed 7 days, ... */ + maCalendarWrapper.loadCalendar( aGregorian, lang::Locale( "en", "US", "")); + } + + SetFirstDate( maCurDate ); + ImplCalendarSelectDate( mpSelectTable.get(), maCurDate, true ); + + // Sonstige Strings erzeugen + maDayText = VclResId(STR_SVT_CALENDAR_DAY); + maWeekText = VclResId(STR_SVT_CALENDAR_WEEK); + + // Tagestexte anlegen + for (sal_Int32 i = 0; i < 31; ++i) + maDayTexts[i] = OUString::number(i+1); + + ImplInitSettings(); +} + +void Calendar::ApplySettings(vcl::RenderContext& rRenderContext) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + maSelColor = rStyleSettings.GetHighlightTextColor(); + SetPointFont(rRenderContext, rStyleSettings.GetToolFont()); + rRenderContext.SetTextColor(rStyleSettings.GetFieldTextColor()); + rRenderContext.SetBackground(Wallpaper(rStyleSettings.GetFieldColor())); +} + +void Calendar::ImplInitSettings() +{ + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + maSelColor = rStyleSettings.GetHighlightTextColor(); + SetPointFont(*GetOutDev(), rStyleSettings.GetToolFont()); + SetTextColor(rStyleSettings.GetFieldTextColor()); + SetBackground(Wallpaper(rStyleSettings.GetFieldColor())); +} + +Calendar::Calendar( vcl::Window* pParent, WinBits nWinStyle ) : + Control( pParent, nWinStyle & (WB_TABSTOP | WB_GROUP | WB_BORDER | WB_3DLOOK) ), + maCalendarWrapper( Application::GetAppLocaleDataWrapper().getComponentContext() ), + maOldFormatFirstDate( 0, 0, 1900 ), + maOldFormatLastDate( 0, 0, 1900 ), + maFirstDate( 0, 0, 1900 ), + maOldFirstDate( 0, 0, 1900 ), + maCurDate( Date::SYSTEM ), + maOldCurDate( 0, 0, 1900 ) +{ + ImplInit( nWinStyle ); +} + +Calendar::~Calendar() +{ + disposeOnce(); +} + +void Calendar::dispose() +{ + mpSelectTable.reset(); + mpOldSelectTable.reset(); + Control::dispose(); +} + +DayOfWeek Calendar::ImplGetWeekStart() const +{ + // Map i18n::Weekdays to Date DayOfWeek + DayOfWeek eDay; + sal_Int16 nDay = maCalendarWrapper.getFirstDayOfWeek(); + switch (nDay) + { + case i18n::Weekdays::SUNDAY : + eDay = SUNDAY; + break; + case i18n::Weekdays::MONDAY : + eDay = MONDAY; + break; + case i18n::Weekdays::TUESDAY : + eDay = TUESDAY; + break; + case i18n::Weekdays::WEDNESDAY : + eDay = WEDNESDAY; + break; + case i18n::Weekdays::THURSDAY : + eDay = THURSDAY; + break; + case i18n::Weekdays::FRIDAY : + eDay = FRIDAY; + break; + case i18n::Weekdays::SATURDAY : + eDay = SATURDAY; + break; + default: + SAL_WARN( "vcl.control", "Calendar::ImplGetWeekStart: broken i18n Gregorian calendar (getFirstDayOfWeek())"); + eDay = SUNDAY; + } + return eDay; +} + +void Calendar::ImplFormat() +{ + if ( !mbFormat ) + return; + + if ( mbCalc ) + { + Size aOutSize = GetOutputSizePixel(); + + if ( (aOutSize.Width() <= 1) || (aOutSize.Height() <= 1) ) + return; + + tools::Long n99TextWidth = GetTextWidth( "99" ); + tools::Long nTextHeight = GetTextHeight(); + + // calculate width and x-position + mnDayWidth = n99TextWidth+DAY_OFFX; + mnMonthWidth = mnDayWidth*7; + mnMonthWidth += MONTH_BORDERX*2; + mnMonthPerLine = aOutSize.Width() / mnMonthWidth; + if ( !mnMonthPerLine ) + mnMonthPerLine = 1; + tools::Long nOver = (aOutSize.Width()-(mnMonthPerLine*mnMonthWidth)) / mnMonthPerLine; + mnMonthWidth += nOver; + mnDaysOffX = MONTH_BORDERX; + mnDaysOffX += nOver/2; + + // calculate height and y-position + mnDayHeight = nTextHeight + DAY_OFFY; + mnWeekDayOffY = nTextHeight + TITLE_OFFY + (TITLE_BORDERY*2); + mnDaysOffY = mnWeekDayOffY + nTextHeight + WEEKDAY_OFFY; + mnMonthHeight = (mnDayHeight*6) + mnDaysOffY; + mnMonthHeight += MONTH_OFFY; + mnLines = aOutSize.Height() / mnMonthHeight; + if ( !mnLines ) + mnLines = 1; + mnMonthHeight += (aOutSize.Height()-(mnLines*mnMonthHeight)) / mnLines; + + // calculate spinfields + tools::Long nSpinSize = nTextHeight+TITLE_BORDERY-SPIN_OFFY; + maPrevRect.SetLeft( SPIN_OFFX ); + maPrevRect.SetTop( SPIN_OFFY ); + maPrevRect.SetRight( maPrevRect.Left()+nSpinSize ); + maPrevRect.SetBottom( maPrevRect.Top()+nSpinSize ); + maNextRect.SetLeft( aOutSize.Width()-SPIN_OFFX-nSpinSize-1 ); + maNextRect.SetTop( SPIN_OFFY ); + maNextRect.SetRight( maNextRect.Left()+nSpinSize ); + maNextRect.SetBottom( maNextRect.Top()+nSpinSize ); + + // Calculate DayOfWeekText (gets displayed in a narrow font) + maDayOfWeekText.clear(); + tools::Long nStartOffX = 0; + sal_Int16 nDay = maCalendarWrapper.getFirstDayOfWeek(); + for ( sal_Int16 nDayOfWeek = 0; nDayOfWeek < 7; nDayOfWeek++ ) + { + // Use narrow name. + OUString aDayOfWeek( maCalendarWrapper.getDisplayName( + i18n::CalendarDisplayIndex::DAY, nDay, 2)); + tools::Long nOffX = (mnDayWidth-GetTextWidth( aDayOfWeek ))/2; + if ( !nDayOfWeek ) + nStartOffX = nOffX; + else + nOffX -= nStartOffX; + nOffX += nDayOfWeek * mnDayWidth; + mnDayOfWeekAry[nDayOfWeek] = nOffX; + maDayOfWeekText += aDayOfWeek; + nDay++; + nDay %= 7; + } + + mbCalc = false; + } + + // calculate number of days + + DayOfWeek eStartDay = ImplGetWeekStart(); + + sal_uInt16 nWeekDay; + Date aTempDate = GetFirstMonth(); + maFirstDate = aTempDate; + nWeekDay = static_cast<sal_uInt16>(aTempDate.GetDayOfWeek()); + nWeekDay = (nWeekDay+(7-static_cast<sal_uInt16>(eStartDay))) % 7; + maFirstDate.AddDays( -nWeekDay ); + mnDayCount = nWeekDay; + sal_uInt16 nDaysInMonth; + sal_uInt16 nMonthCount = static_cast<sal_uInt16>(mnMonthPerLine*mnLines); + for ( sal_uInt16 i = 0; i < nMonthCount; i++ ) + { + nDaysInMonth = aTempDate.GetDaysInMonth(); + mnDayCount += nDaysInMonth; + aTempDate.AddDays( nDaysInMonth ); + } + Date aTempDate2 = aTempDate; + --aTempDate2; + nDaysInMonth = aTempDate2.GetDaysInMonth(); + aTempDate2.AddDays( -(nDaysInMonth-1) ); + nWeekDay = static_cast<sal_uInt16>(aTempDate2.GetDayOfWeek()); + nWeekDay = (nWeekDay+(7-static_cast<sal_uInt16>(eStartDay))) % 7; + mnDayCount += 42-nDaysInMonth-nWeekDay; + + // determine colours + maOtherColor = COL_LIGHTGRAY; + if ( maOtherColor.IsRGBEqual( GetBackground().GetColor() ) ) + maOtherColor = COL_GRAY; + + Date aLastDate = GetLastDate(); + if ( (maOldFormatLastDate != aLastDate) || + (maOldFormatFirstDate != maFirstDate) ) + { + maOldFormatFirstDate = maFirstDate; + maOldFormatLastDate = aLastDate; + } + + // get DateInfo + sal_Int16 nNewFirstYear = maFirstDate.GetYear(); + sal_Int16 nNewLastYear = GetLastDate().GetYear(); + if ( mnFirstYear ) + { + if ( nNewFirstYear < mnFirstYear ) + { + mnFirstYear = nNewFirstYear; + } + if ( nNewLastYear > mnLastYear ) + { + mnLastYear = nNewLastYear; + } + } + else + { + mnFirstYear = nNewFirstYear; + mnLastYear = nNewLastYear; + } + + mbFormat = false; +} + +sal_uInt16 Calendar::ImplDoHitTest( const Point& rPos, Date& rDate ) const +{ + if ( mbFormat ) + return 0; + + if ( maPrevRect.Contains( rPos ) ) + return CALENDAR_HITTEST_PREV; + else if ( maNextRect.Contains( rPos ) ) + return CALENDAR_HITTEST_NEXT; + + tools::Long nY; + tools::Long nOffX; + sal_Int32 nDay; + DayOfWeek eStartDay = ImplGetWeekStart(); + + rDate = GetFirstMonth(); + nY = 0; + for ( tools::Long i = 0; i < mnLines; i++ ) + { + if ( rPos.Y() < nY ) + return 0; + + tools::Long nX = 0; + tools::Long nYMonth = nY+mnMonthHeight; + for ( tools::Long j = 0; j < mnMonthPerLine; j++ ) + { + if ( (rPos.X() < nX) && (rPos.Y() < nYMonth) ) + return 0; + + sal_uInt16 nDaysInMonth = rDate.GetDaysInMonth(); + + // matching month was found + if ( (rPos.X() > nX) && (rPos.Y() < nYMonth) && + (rPos.X() < nX+mnMonthWidth) ) + { + if ( rPos.Y() < (nY+(TITLE_BORDERY*2)+mnDayHeight)) + return CALENDAR_HITTEST_MONTHTITLE; + else + { + tools::Long nDayX = nX+mnDaysOffX; + tools::Long nDayY = nY+mnDaysOffY; + if ( rPos.Y() < nDayY ) + return 0; + sal_Int32 nDayIndex = static_cast<sal_Int32>(rDate.GetDayOfWeek()); + nDayIndex = (nDayIndex+(7-static_cast<sal_Int32>(eStartDay))) % 7; + if ( (i == 0) && (j == 0) ) + { + Date aTempDate = rDate; + aTempDate.AddDays( -nDayIndex ); + for ( nDay = 0; nDay < nDayIndex; nDay++ ) + { + nOffX = nDayX + (nDay*mnDayWidth); + if ( (rPos.Y() >= nDayY) && (rPos.Y() < nDayY+mnDayHeight) && + (rPos.X() >= nOffX) && (rPos.X() < nOffX+mnDayWidth) ) + { + rDate = aTempDate; + rDate.AddDays( nDay ); + return CALENDAR_HITTEST_DAY; + } + } + } + for ( nDay = 1; nDay <= nDaysInMonth; nDay++ ) + { + if ( rPos.Y() < nDayY ) + { + rDate.AddDays( nDayIndex ); + return 0; + } + nOffX = nDayX + (nDayIndex*mnDayWidth); + if ( (rPos.Y() >= nDayY) && (rPos.Y() < nDayY+mnDayHeight) && + (rPos.X() >= nOffX) && (rPos.X() < nOffX+mnDayWidth) ) + { + rDate.AddDays( nDay-1 ); + return CALENDAR_HITTEST_DAY; + } + if ( nDayIndex == 6 ) + { + nDayIndex = 0; + nDayY += mnDayHeight; + } + else + nDayIndex++; + } + if ( (i == mnLines-1) && (j == mnMonthPerLine-1) ) + { + sal_uInt16 nWeekDay = static_cast<sal_uInt16>(rDate.GetDayOfWeek()); + nWeekDay = (nWeekDay+(7-static_cast<sal_uInt16>(eStartDay))) % 7; + sal_Int32 nDayCount = 42-nDaysInMonth-nWeekDay; + Date aTempDate = rDate; + aTempDate.AddDays( nDaysInMonth ); + for ( nDay = 1; nDay <= nDayCount; nDay++ ) + { + if ( rPos.Y() < nDayY ) + { + rDate.AddDays( nDayIndex ); + return 0; + } + nOffX = nDayX + (nDayIndex*mnDayWidth); + if ( (rPos.Y() >= nDayY) && (rPos.Y() < nDayY+mnDayHeight) && + (rPos.X() >= nOffX) && (rPos.X() < nOffX+mnDayWidth) ) + { + rDate = aTempDate; + rDate.AddDays( nDay-1 ); + return CALENDAR_HITTEST_DAY; + } + if ( nDayIndex == 6 ) + { + nDayIndex = 0; + nDayY += mnDayHeight; + } + else + nDayIndex++; + } + } + } + } + + rDate.AddDays( nDaysInMonth ); + nX += mnMonthWidth; + } + + nY += mnMonthHeight; + } + + return 0; +} + +namespace +{ + +void ImplDrawSpinArrow(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect, bool bPrev) +{ + tools::Long i; + tools::Long n; + tools::Long nLines; + tools::Long nHeight = rRect.GetHeight(); + tools::Long nWidth = rRect.GetWidth(); + if (nWidth < nHeight) + n = nWidth; + else + n = nHeight; + if (!(n & 0x01)) + n--; + nLines = n/2; + + tools::Rectangle aRect(Point( rRect.Left() + (nWidth / 2) - (nLines / 2), + rRect.Top() + (nHeight / 2) ), + Size(1, 1)); + if (!bPrev) + { + aRect.AdjustLeft(nLines ); + aRect.AdjustRight(nLines ); + } + + rRenderContext.DrawRect(aRect); + for (i = 0; i < nLines; i++) + { + if (bPrev) + { + aRect.AdjustLeft( 1 ); + aRect.AdjustRight( 1 ); + } + else + { + aRect.AdjustLeft( -1 ); + aRect.AdjustRight( -1 ); + } + aRect.AdjustTop( -1 ); + aRect.AdjustBottom( 1 ); + rRenderContext.DrawRect(aRect); + } +} + +} //end anonymous namespace + +void Calendar::ImplDrawSpin(vcl::RenderContext& rRenderContext ) +{ + rRenderContext.SetLineColor(); + rRenderContext.SetFillColor(rRenderContext.GetSettings().GetStyleSettings().GetButtonTextColor()); + tools::Rectangle aOutRect = maPrevRect; + aOutRect.AdjustLeft(3 ); + aOutRect.AdjustTop(3 ); + aOutRect.AdjustRight( -3 ); + aOutRect.AdjustBottom( -3 ); + ImplDrawSpinArrow(rRenderContext, aOutRect, true); + aOutRect = maNextRect; + aOutRect.AdjustLeft(3 ); + aOutRect.AdjustTop(3 ); + aOutRect.AdjustRight( -3 ); + aOutRect.AdjustBottom( -3 ); + ImplDrawSpinArrow(rRenderContext, aOutRect, false); +} + +void Calendar::ImplDrawDate(vcl::RenderContext& rRenderContext, + tools::Long nX, tools::Long nY, + sal_uInt16 nDay, sal_uInt16 nMonth, sal_Int16 nYear, + bool bOther, sal_Int32 nToday ) +{ + Color const * pTextColor = nullptr; + const OUString& rDay = maDayTexts[nDay - 1]; + tools::Rectangle aDateRect(nX, nY, nX + mnDayWidth - 1, nY + mnDayHeight - 1); + + bool bSel = false; + bool bFocus = false; + // actual day + if ((nDay == maCurDate.GetDay()) && + (nMonth == maCurDate.GetMonth()) && + (nYear == maCurDate.GetYear())) + { + bFocus = true; + } + if (mpSelectTable) + { + if (mpSelectTable->find(Date(nDay, nMonth, nYear).GetDate()) != mpSelectTable->end()) + bSel = true; + } + + // get textcolour + if (bSel) + pTextColor = &maSelColor; + else if (bOther) + pTextColor = &maOtherColor; + + if (bFocus) + HideFocus(); + + // display background + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + if (bSel) + { + rRenderContext.SetLineColor(); + rRenderContext.SetFillColor(rStyleSettings.GetHighlightColor()); + rRenderContext.DrawRect(aDateRect); + } + + // display text + tools::Long nTextX = nX + (mnDayWidth - GetTextWidth(rDay)) - (DAY_OFFX / 2); + tools::Long nTextY = nY + (mnDayHeight - GetTextHeight()) / 2; + if (pTextColor) + { + Color aOldColor = rRenderContext.GetTextColor(); + rRenderContext.SetTextColor(*pTextColor); + rRenderContext.DrawText(Point(nTextX, nTextY), rDay); + rRenderContext.SetTextColor(aOldColor); + } + else + rRenderContext.DrawText(Point(nTextX, nTextY), rDay); + + // today + Date aTodayDate(maCurDate); + if (nToday) + aTodayDate.SetDate(nToday); + else + aTodayDate = Date(Date::SYSTEM); + if ((nDay == aTodayDate.GetDay()) && + (nMonth == aTodayDate.GetMonth()) && + (nYear == aTodayDate.GetYear())) + { + rRenderContext.SetLineColor(rStyleSettings.GetWindowTextColor()); + rRenderContext.SetFillColor(); + rRenderContext.DrawRect(aDateRect); + } + + // if needed do FocusRect + if (bFocus && HasFocus()) + ShowFocus(aDateRect); +} + +void Calendar::ImplDraw(vcl::RenderContext& rRenderContext) +{ + ImplFormat(); + + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + Size aOutSize(GetOutputSizePixel()); + tools::Long i; + tools::Long j; + tools::Long nY; + tools::Long nDeltaX; + tools::Long nDeltaY; + tools::Long nDayX; + tools::Long nDayY; + sal_Int32 nToday = Date(Date::SYSTEM).GetDate(); + sal_uInt16 nDay; + sal_uInt16 nMonth; + sal_Int16 nYear; + Date aDate = GetFirstMonth(); + DayOfWeek eStartDay = ImplGetWeekStart(); + + HideFocus(); + + nY = 0; + for (i = 0; i < mnLines; i++) + { + // display title bar + rRenderContext.SetLineColor(); + rRenderContext.SetFillColor(rStyleSettings.GetFaceColor()); + tools::Rectangle aTitleRect(0, nY, aOutSize.Width() - 1, nY + mnDayHeight - DAY_OFFY + TITLE_BORDERY * 2); + rRenderContext.DrawRect(aTitleRect); + Point aTopLeft1(aTitleRect.Left(), aTitleRect.Top()); + Point aTopLeft2(aTitleRect.Left(), aTitleRect.Top() + 1); + Point aBottomRight1(aTitleRect.Right(), aTitleRect.Bottom()); + Point aBottomRight2(aTitleRect.Right(), aTitleRect.Bottom() - 1); + rRenderContext.SetLineColor(rStyleSettings.GetDarkShadowColor()); + rRenderContext.DrawLine(aTopLeft1, Point(aBottomRight1.X(), aTopLeft1.Y())); + rRenderContext.SetLineColor(rStyleSettings.GetLightColor() ); + rRenderContext.DrawLine(aTopLeft2, Point(aBottomRight2.X(), aTopLeft2.Y())); + rRenderContext.DrawLine(aTopLeft2, Point(aTopLeft2.X(), aBottomRight2.Y())); + rRenderContext.SetLineColor(rStyleSettings.GetShadowColor() ); + rRenderContext.DrawLine(Point(aTopLeft2.X(), aBottomRight2.Y()), aBottomRight2); + rRenderContext.DrawLine(Point(aBottomRight2.X(), aTopLeft2.Y()), aBottomRight2); + rRenderContext.SetLineColor(rStyleSettings.GetDarkShadowColor()); + rRenderContext.DrawLine(Point(aTopLeft1.X(), aBottomRight1.Y()), aBottomRight1); + Point aSepPos1(0, aTitleRect.Top() + TITLE_BORDERY); + Point aSepPos2(0, aTitleRect.Bottom() - TITLE_BORDERY); + for (j = 0; j < mnMonthPerLine-1; j++) + { + aSepPos1.AdjustX(mnMonthWidth-1 ); + aSepPos2.setX( aSepPos1.X() ); + rRenderContext.SetLineColor(rStyleSettings.GetShadowColor()); + rRenderContext.DrawLine(aSepPos1, aSepPos2); + aSepPos1.AdjustX( 1 ); + aSepPos2.setX( aSepPos1.X() ); + rRenderContext.SetLineColor(rStyleSettings.GetLightColor()); + rRenderContext.DrawLine(aSepPos1, aSepPos2); + } + + tools::Long nX = 0; + for (j = 0; j < mnMonthPerLine; j++) + { + nMonth = aDate.GetMonth(); + nYear = aDate.GetYear(); + + // display month in title bar + nDeltaX = nX; + nDeltaY = nY + TITLE_BORDERY; + OUString aMonthText = maCalendarWrapper.getDisplayName(i18n::CalendarDisplayIndex::MONTH, nMonth - 1, 1) + + " " + + OUString::number(nYear); + tools::Long nMonthTextWidth = rRenderContext.GetTextWidth(aMonthText); + tools::Long nMonthOffX1 = 0; + tools::Long nMonthOffX2 = 0; + if (i == 0) + { + if (j == 0) + nMonthOffX1 = maPrevRect.Right() + 1; + if (j == mnMonthPerLine - 1) + nMonthOffX2 = aOutSize.Width() - maNextRect.Left() + 1; + } + tools::Long nMaxMonthWidth = mnMonthWidth - nMonthOffX1 - nMonthOffX2 - 4; + if (nMonthTextWidth > nMaxMonthWidth) + { + // Abbreviated month name. + aMonthText = maCalendarWrapper.getDisplayName(i18n::CalendarDisplayIndex::MONTH, nMonth - 1, 0) + + " " + + OUString::number(nYear); + nMonthTextWidth = rRenderContext.GetTextWidth(aMonthText); + } + tools::Long nTempOff = (mnMonthWidth - nMonthTextWidth + 1) / 2; + if (nTempOff < nMonthOffX1) + nDeltaX += nMonthOffX1 + 1; + else + { + if (nTempOff + nMonthTextWidth > mnMonthWidth - nMonthOffX2) + nDeltaX += mnMonthWidth - nMonthOffX2 - nMonthTextWidth; + else + nDeltaX += nTempOff; + } + rRenderContext.SetTextColor(rStyleSettings.GetButtonTextColor()); + rRenderContext.DrawText(Point(nDeltaX, nDeltaY), aMonthText); + rRenderContext.SetTextColor(rStyleSettings.GetWindowTextColor()); + + // display week bar + nDayX = nX + mnDaysOffX; + nDayY = nY + mnWeekDayOffY; + nDeltaY = nDayY + mnDayHeight; + rRenderContext.SetLineColor(rStyleSettings.GetWindowTextColor()); + Point aStartPos(nDayX, nDeltaY); + rRenderContext.DrawLine(aStartPos, Point(nDayX + (7 * mnDayWidth), nDeltaY)); + std::vector<sal_Int32> aTmp; + for (int k=0; k<6; ++k) + aTmp.push_back(mnDayOfWeekAry[k+1]); + rRenderContext.DrawTextArray(Point(nDayX + mnDayOfWeekAry[0], nDayY), maDayOfWeekText, aTmp); + + // display days + sal_uInt16 nDaysInMonth = aDate.GetDaysInMonth(); + nDayX = nX + mnDaysOffX; + nDayY = nY + mnDaysOffY; + sal_uInt16 nDayIndex = static_cast<sal_uInt16>(aDate.GetDayOfWeek()); + nDayIndex = (nDayIndex + (7 - static_cast<sal_uInt16>(eStartDay))) % 7; + if (i == 0 && j == 0) + { + Date aTempDate = aDate; + aTempDate.AddDays( -nDayIndex ); + for (nDay = 0; nDay < nDayIndex; ++nDay) + { + nDeltaX = nDayX + (nDay * mnDayWidth); + ImplDrawDate(rRenderContext, nDeltaX, nDayY, nDay + aTempDate.GetDay(), + aTempDate.GetMonth(), aTempDate.GetYear(), + true, nToday); + } + } + for (nDay = 1; nDay <= nDaysInMonth; nDay++) + { + nDeltaX = nDayX + (nDayIndex * mnDayWidth); + ImplDrawDate(rRenderContext, nDeltaX, nDayY, nDay, nMonth, nYear, + false, nToday); + if (nDayIndex == 6) + { + nDayIndex = 0; + nDayY += mnDayHeight; + } + else + nDayIndex++; + } + if ((i == mnLines - 1) && (j == mnMonthPerLine - 1)) + { + sal_uInt16 nWeekDay = static_cast<sal_uInt16>(aDate.GetDayOfWeek()); + nWeekDay = (nWeekDay + (7 - static_cast<sal_uInt16>(eStartDay))) % 7; + sal_uInt16 nDayCount = 42 - nDaysInMonth - nWeekDay; + Date aTempDate = aDate; + aTempDate.AddDays( nDaysInMonth ); + for (nDay = 1; nDay <= nDayCount; ++nDay) + { + nDeltaX = nDayX + (nDayIndex * mnDayWidth); + ImplDrawDate(rRenderContext, nDeltaX, nDayY, nDay, + aTempDate.GetMonth(), aTempDate.GetYear(), + true, nToday); + if (nDayIndex == 6) + { + nDayIndex = 0; + nDayY += mnDayHeight; + } + else + nDayIndex++; + } + } + + aDate.AddDays( nDaysInMonth ); + nX += mnMonthWidth; + } + + nY += mnMonthHeight; + } + + // draw spin buttons + ImplDrawSpin(rRenderContext); +} + +void Calendar::ImplUpdateDate( const Date& rDate ) +{ + if (IsReallyVisible() && IsUpdateMode()) + { + tools::Rectangle aDateRect(GetDateRect(rDate)); + if (!aDateRect.IsEmpty()) + { + Invalidate(aDateRect); + } + } +} + +void Calendar::ImplUpdateSelection( IntDateSet* pOld ) +{ + IntDateSet* pNew = mpSelectTable.get(); + + for (auto const& nKey : *pOld) + { + if ( pNew->find(nKey) == pNew->end() ) + { + Date aTempDate(nKey); + ImplUpdateDate(aTempDate); + } + } + + for (auto const& nKey : *pNew) + { + if ( pOld->find(nKey) == pOld->end() ) + { + Date aTempDate(nKey); + ImplUpdateDate(aTempDate); + } + } +} + +void Calendar::ImplMouseSelect( const Date& rDate, sal_uInt16 nHitTest ) +{ + IntDateSet aOldSel( *mpSelectTable ); + Date aOldDate = maCurDate; + Date aTempDate = rDate; + + if ( !(nHitTest & CALENDAR_HITTEST_DAY) ) + --aTempDate; + + if ( !(nHitTest & CALENDAR_HITTEST_DAY) ) + aTempDate = maOldCurDate; + if ( aTempDate != maCurDate ) + { + maCurDate = aTempDate; + ImplCalendarSelectDate( mpSelectTable.get(), aOldDate, false ); + ImplCalendarSelectDate( mpSelectTable.get(), maCurDate, true ); + } + + bool bNewSel = aOldSel != *mpSelectTable; + if ( (maCurDate != aOldDate) || bNewSel ) + { + HideFocus(); + if ( bNewSel ) + ImplUpdateSelection( &aOldSel ); + if ( !bNewSel || aOldSel.find( aOldDate.GetDate() ) == aOldSel.end() ) + ImplUpdateDate( aOldDate ); + // assure focus rectangle is displayed again + if ( HasFocus() || !bNewSel + || mpSelectTable->find( maCurDate.GetDate() ) == mpSelectTable->end() ) + ImplUpdateDate( maCurDate ); + } +} + +void Calendar::ImplUpdate( bool bCalcNew ) +{ + if (IsReallyVisible() && IsUpdateMode()) + { + if (bCalcNew && !mbCalc) + { + Invalidate(); + } + else if (!mbFormat && !mbCalc) + { + Invalidate(); + } + } + + if (bCalcNew) + mbCalc = true; + mbFormat = true; +} + +void Calendar::ImplScroll( bool bPrev ) +{ + Date aNewFirstMonth = GetFirstMonth(); + if ( bPrev ) + { + --aNewFirstMonth; + aNewFirstMonth.AddDays( -(aNewFirstMonth.GetDaysInMonth()-1)); + } + else + aNewFirstMonth.AddDays( aNewFirstMonth.GetDaysInMonth()); + SetFirstDate( aNewFirstMonth ); +} + +void Calendar::ImplShowMenu( const Point& rPos, const Date& rDate ) +{ + EndSelection(); + + Date aOldFirstDate = GetFirstMonth(); + ScopedVclPtrInstance<PopupMenu> aPopupMenu; + sal_uInt16 nMonthOff; + sal_uInt16 nCurItemId; + sal_uInt16 nYear = rDate.GetYear()-1; + sal_uInt16 i; + sal_uInt16 j; + sal_uInt16 nYearIdCount = 1000; + + nMonthOff = (rDate.GetYear()-aOldFirstDate.GetYear())*12; + if ( aOldFirstDate.GetMonth() < rDate.GetMonth() ) + nMonthOff += rDate.GetMonth()-aOldFirstDate.GetMonth(); + else + nMonthOff -= aOldFirstDate.GetMonth()-rDate.GetMonth(); + + // construct menu (include years with different months) + for ( i = 0; i < MENU_YEAR_COUNT; i++ ) + { + VclPtrInstance<PopupMenu> pYearPopupMenu; + for ( j = 1; j <= 12; j++ ) + pYearPopupMenu->InsertItem( nYearIdCount+j, + maCalendarWrapper.getDisplayName( + i18n::CalendarDisplayIndex::MONTH, j-1, 1)); + aPopupMenu->InsertItem( 10+i, OUString::number( nYear+i ) ); + aPopupMenu->SetPopupMenu( 10+i, pYearPopupMenu ); + nYearIdCount += 1000; + } + + mbMenuDown = true; + nCurItemId = aPopupMenu->Execute( this, rPos ); + mbMenuDown = false; + + if ( !nCurItemId ) + return; + + sal_uInt16 nTempMonthOff = nMonthOff % 12; + sal_uInt16 nTempYearOff = nMonthOff / 12; + sal_uInt16 nNewMonth = nCurItemId % 1000; + sal_uInt16 nNewYear = nYear+((nCurItemId-1000)/1000); + if ( nTempMonthOff < nNewMonth ) + nNewMonth = nNewMonth - nTempMonthOff; + else + { + nNewYear--; + nNewMonth = 12-(nTempMonthOff-nNewMonth); + } + nNewYear = nNewYear - nTempYearOff; + SetFirstDate( Date( 1, nNewMonth, nNewYear ) ); +} + +void Calendar::ImplTracking( const Point& rPos, bool bRepeat ) +{ + Date aTempDate = maCurDate; + sal_uInt16 nHitTest = ImplDoHitTest( rPos, aTempDate ); + + if ( mbSpinDown ) + { + mbPrevIn = (nHitTest & CALENDAR_HITTEST_PREV) != 0; + mbNextIn = (nHitTest & CALENDAR_HITTEST_NEXT) != 0; + + if ( bRepeat && (mbPrevIn || mbNextIn) ) + { + ImplScroll( mbPrevIn ); + } + } + else + ImplMouseSelect( aTempDate, nHitTest ); +} + +void Calendar::ImplEndTracking( bool bCancel ) +{ + bool bSelection = false; + bool bSpinDown = mbSpinDown; + + mbDrag = false; + mbSpinDown = false; + mbPrevIn = false; + mbNextIn = false; + + if ( bCancel ) + { + if ( maOldFirstDate != maFirstDate ) + SetFirstDate( maOldFirstDate ); + + if ( !bSpinDown ) + { + IntDateSet aOldSel( *mpSelectTable ); + Date aOldDate = maCurDate; + maCurDate = maOldCurDate; + *mpSelectTable = *mpOldSelectTable; + HideFocus(); + ImplUpdateSelection( &aOldSel ); + if ( aOldSel.find( aOldDate.GetDate() ) == aOldSel.end() ) + ImplUpdateDate( aOldDate ); + // assure focus rectangle is displayed again + if ( HasFocus() || mpSelectTable->find( maCurDate.GetDate() ) == mpSelectTable->end() ) + ImplUpdateDate( maCurDate ); + } + } + + if ( bSpinDown ) + return; + + if ( !bCancel ) + { + // determine if we should scroll the visible area + if ( !mpSelectTable->empty() ) + { + Date aFirstSelDate( *mpSelectTable->begin() ); + Date aLastSelDate( *mpSelectTable->rbegin() ); + if ( aLastSelDate < GetFirstMonth() ) + ImplScroll( true ); + else if ( GetLastMonth() < aFirstSelDate ) + ImplScroll( false ); + } + } + + if ( !bCancel && ((maCurDate != maOldCurDate) || (*mpOldSelectTable != *mpSelectTable)) ) + Select(); + + if ( !bSelection && (mnWinStyle & WB_TABSTOP) && !bCancel ) + GrabFocus(); + + mpOldSelectTable.reset(); +} + +void Calendar::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if ( rMEvt.IsLeft() && !mbMenuDown ) + { + Date aTempDate = maCurDate; + sal_uInt16 nHitTest = ImplDoHitTest( rMEvt.GetPosPixel(), aTempDate ); + if ( nHitTest ) + { + if ( nHitTest & CALENDAR_HITTEST_MONTHTITLE ) + ImplShowMenu( rMEvt.GetPosPixel(), aTempDate ); + else + { + maOldFirstDate = maFirstDate; + + mbPrevIn = (nHitTest & CALENDAR_HITTEST_PREV) != 0; + mbNextIn = (nHitTest & CALENDAR_HITTEST_NEXT) != 0; + if ( mbPrevIn || mbNextIn ) + { + mbSpinDown = true; + ImplScroll( mbPrevIn ); + // it should really read BUTTONREPEAT, therefore do not + // change it to SCROLLREPEAT, check with TH, + // why it could be different (71775) + StartTracking( StartTrackingFlags::ButtonRepeat ); + } + else + { + if ( (rMEvt.GetClicks() != 2) || !(nHitTest & CALENDAR_HITTEST_DAY) ) + { + maOldCurDate = maCurDate; + mpOldSelectTable.reset(new IntDateSet( *mpSelectTable )); + + mbDrag = true; + StartTracking(); + + ImplMouseSelect( aTempDate, nHitTest ); + } + if (rMEvt.GetClicks() == 2) + maActivateHdl.Call(this); + } + } + } + + return; + } + + Control::MouseButtonDown( rMEvt ); +} + +void Calendar::Tracking( const TrackingEvent& rTEvt ) +{ + Point aMousePos = rTEvt.GetMouseEvent().GetPosPixel(); + + if ( rTEvt.IsTrackingEnded() ) + ImplEndTracking( rTEvt.IsTrackingCanceled() ); + else + ImplTracking( aMousePos, rTEvt.IsTrackingRepeat() ); +} + +void Calendar::KeyInput( const KeyEvent& rKEvt ) +{ + Date aNewDate = maCurDate; + + switch ( rKEvt.GetKeyCode().GetCode() ) + { + case KEY_HOME: + aNewDate.SetDay( 1 ); + break; + + case KEY_END: + aNewDate.SetDay( aNewDate.GetDaysInMonth() ); + break; + + case KEY_LEFT: + --aNewDate; + break; + + case KEY_RIGHT: + ++aNewDate; + break; + + case KEY_UP: + aNewDate.AddDays( -7 ); + break; + + case KEY_DOWN: + aNewDate.AddDays( 7 ); + break; + + case KEY_PAGEUP: + { + Date aTempDate = aNewDate; + aTempDate.AddDays( -(aNewDate.GetDay()+1) ); + aNewDate.AddDays( -aTempDate.GetDaysInMonth() ); + } + break; + + case KEY_PAGEDOWN: + aNewDate.AddDays( aNewDate.GetDaysInMonth() ); + break; + + case KEY_RETURN: + break; + + default: + Control::KeyInput( rKEvt ); + break; + } + + if ( aNewDate != maCurDate ) + { + SetCurDate( aNewDate ); + Select(); + } + + if (rKEvt.GetKeyCode().GetCode() == KEY_RETURN) + { + if (maActivateHdl.IsSet()) + maActivateHdl.Call(this); + else + Control::KeyInput(rKEvt); + } +} + +void Calendar::Paint( vcl::RenderContext& rRenderContext, const tools::Rectangle& ) +{ + ImplDraw(rRenderContext); +} + +void Calendar::GetFocus() +{ + ImplUpdateDate( maCurDate ); + Control::GetFocus(); +} + +void Calendar::LoseFocus() +{ + HideFocus(); + Control::LoseFocus(); +} + +void Calendar::Resize() +{ + ImplUpdate( true ); + Control::Resize(); +} + +void Calendar::RequestHelp( const HelpEvent& rHEvt ) +{ + if ( rHEvt.GetMode() & (HelpEventMode::QUICK | HelpEventMode::BALLOON) ) + { + Date aDate = maCurDate; + if ( GetDate( ScreenToOutputPixel( rHEvt.GetMousePosPixel() ), aDate ) ) + { + tools::Rectangle aDateRect = GetDateRect( aDate ); + Point aPt = OutputToScreenPixel( aDateRect.TopLeft() ); + aDateRect.SetLeft( aPt.X() ); + aDateRect.SetTop( aPt.Y() ); + aPt = OutputToScreenPixel( aDateRect.BottomRight() ); + aDateRect.SetRight( aPt.X() ); + aDateRect.SetBottom( aPt.Y() ); + + if ( rHEvt.GetMode() & HelpEventMode::QUICK ) + { + maCalendarWrapper.setGregorianDateTime( aDate); + sal_uInt16 nWeek = static_cast<sal_uInt16>(maCalendarWrapper.getValue( i18n::CalendarFieldIndex::WEEK_OF_YEAR)); + sal_uInt16 nMonth = aDate.GetMonth(); + OUString aStr = maDayText + + ": " + + OUString::number(aDate.GetDayOfYear()) + + " / " + + maWeekText + + ": " + + OUString::number(nWeek); + // if year is not the same, add it + if ( (nMonth == 12) && (nWeek == 1) ) + { + aStr += ", " + OUString::number(aDate.GetNextYear()); + } + else if ( (nMonth == 1) && (nWeek > 50) ) + { + aStr += ", " + OUString::number(aDate.GetYear()-1); + } + Help::ShowQuickHelp( this, aDateRect, aStr ); + return; + } + } + } + + Control::RequestHelp( rHEvt ); +} + +void Calendar::Command( const CommandEvent& rCEvt ) +{ + if ( rCEvt.GetCommand() == CommandEventId::ContextMenu ) + { + if ( rCEvt.IsMouseEvent() ) + { + Date aTempDate = maCurDate; + sal_uInt16 nHitTest = ImplDoHitTest( rCEvt.GetMousePosPixel(), aTempDate ); + if ( nHitTest & CALENDAR_HITTEST_MONTHTITLE ) + { + ImplShowMenu( rCEvt.GetMousePosPixel(), aTempDate ); + return; + } + } + } + else if ( rCEvt.GetCommand() == CommandEventId::Wheel ) + { + const CommandWheelData* pData = rCEvt.GetWheelData(); + if ( pData->GetMode() == CommandWheelMode::SCROLL ) + { + tools::Long nNotchDelta = pData->GetNotchDelta(); + if ( nNotchDelta < 0 ) + { + while ( nNotchDelta < 0 ) + { + ImplScroll( true ); + nNotchDelta++; + } + } + else + { + while ( nNotchDelta > 0 ) + { + ImplScroll( false ); + nNotchDelta--; + } + } + + return; + } + } + + Control::Command( rCEvt ); +} + +void Calendar::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( nType == StateChangedType::InitShow ) + ImplFormat(); +} + +void Calendar::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ImplInitSettings(); + Invalidate(); + } +} + +void Calendar::Select() +{ + maSelectHdl.Call( this ); +} + +Date Calendar::GetFirstSelectedDate() const +{ + if ( !mpSelectTable->empty() ) + return Date( *mpSelectTable->begin() ); + else + { + Date aDate( 0, 0, 0 ); + return aDate; + } +} + +void Calendar::SetCurDate( const Date& rNewDate ) +{ + if ( !rNewDate.IsValidAndGregorian() ) + return; + + if ( maCurDate == rNewDate ) + return; + + bool bUpdate = IsVisible() && IsUpdateMode(); + Date aOldDate = maCurDate; + maCurDate = rNewDate; + + ImplCalendarSelectDate( mpSelectTable.get(), aOldDate, false ); + ImplCalendarSelectDate( mpSelectTable.get(), maCurDate, true ); + + // shift actual date in the visible area + if ( mbFormat || (maCurDate < GetFirstMonth()) ) + SetFirstDate( maCurDate ); + else if ( maCurDate > GetLastMonth() ) + { + Date aTempDate = GetLastMonth(); + tools::Long nDateOff = maCurDate-aTempDate; + if ( nDateOff < 365 ) + { + Date aFirstDate = GetFirstMonth(); + aFirstDate.AddDays( aFirstDate.GetDaysInMonth() ); + ++aTempDate; + while ( nDateOff > aTempDate.GetDaysInMonth() ) + { + aFirstDate.AddDays( aFirstDate.GetDaysInMonth() ); + sal_Int32 nDaysInMonth = aTempDate.GetDaysInMonth(); + aTempDate.AddDays( nDaysInMonth ); + nDateOff -= nDaysInMonth; + } + SetFirstDate( aFirstDate ); + } + else + SetFirstDate( maCurDate ); + } + else + { + if ( bUpdate ) + { + HideFocus(); + ImplUpdateDate( aOldDate ); + ImplUpdateDate( maCurDate ); + } + } +} + +void Calendar::SetFirstDate( const Date& rNewFirstDate ) +{ + if ( maFirstDate != rNewFirstDate ) + { + maFirstDate = Date( 1, rNewFirstDate.GetMonth(), rNewFirstDate.GetYear() ); + ImplUpdate(); + } +} + +Date Calendar::GetFirstMonth() const +{ + if ( maFirstDate.GetDay() > 1 ) + { + if ( maFirstDate.GetMonth() == 12 ) + return Date( 1, 1, maFirstDate.GetNextYear() ); + else + return Date( 1, maFirstDate.GetMonth()+1, maFirstDate.GetYear() ); + } + else + return maFirstDate; +} + +Date Calendar::GetLastMonth() const +{ + Date aDate = GetFirstMonth(); + sal_uInt16 nMonthCount = GetMonthCount(); + for ( sal_uInt16 i = 0; i < nMonthCount; i++ ) + aDate.AddDays( aDate.GetDaysInMonth() ); + --aDate; + return aDate; +} + +sal_uInt16 Calendar::GetMonthCount() const +{ + if ( mbFormat ) + return 1; + else + return static_cast<sal_uInt16>(mnMonthPerLine*mnLines); +} + +bool Calendar::GetDate( const Point& rPos, Date& rDate ) const +{ + Date aDate = maCurDate; + sal_uInt16 nHitTest = ImplDoHitTest( rPos, aDate ); + if ( nHitTest & CALENDAR_HITTEST_DAY ) + { + rDate = aDate; + return true; + } + else + return false; +} + +tools::Rectangle Calendar::GetDateRect( const Date& rDate ) const +{ + tools::Rectangle aRect; + + if ( mbFormat || (rDate < maFirstDate) || (rDate > (maFirstDate+mnDayCount)) ) + return aRect; + + tools::Long nX; + tools::Long nY; + sal_Int32 nDaysOff; + sal_uInt16 nDayIndex; + Date aDate = GetFirstMonth(); + + if ( rDate < aDate ) + { + aRect = GetDateRect( aDate ); + nDaysOff = aDate-rDate; + nX = nDaysOff*mnDayWidth; + aRect.AdjustLeft( -nX ); + aRect.AdjustRight( -nX ); + return aRect; + } + else + { + Date aLastDate = GetLastMonth(); + if ( rDate > aLastDate ) + { + sal_Int32 nWeekDay = static_cast<sal_Int32>(aLastDate.GetDayOfWeek()); + nWeekDay = (nWeekDay+(7-ImplGetWeekStart())) % 7; + aLastDate.AddDays( -nWeekDay ); + aRect = GetDateRect( aLastDate ); + nDaysOff = rDate-aLastDate; + nDayIndex = 0; + for ( sal_Int32 i = 0; i <= nDaysOff; i++ ) + { + if ( aLastDate == rDate ) + { + aRect.AdjustLeft(nDayIndex*mnDayWidth ); + aRect.SetRight( aRect.Left()+mnDayWidth ); + return aRect; + } + if ( nDayIndex == 6 ) + { + nDayIndex = 0; + aRect.AdjustTop(mnDayHeight ); + aRect.AdjustBottom(mnDayHeight ); + } + else + nDayIndex++; + ++aLastDate; + } + } + } + + nY = 0; + for ( tools::Long i = 0; i < mnLines; i++ ) + { + nX = 0; + for ( tools::Long j = 0; j < mnMonthPerLine; j++ ) + { + sal_uInt16 nDaysInMonth = aDate.GetDaysInMonth(); + + // month is called + if ( (aDate.GetMonth() == rDate.GetMonth()) && + (aDate.GetYear() == rDate.GetYear()) ) + { + tools::Long nDayX = nX+mnDaysOffX; + tools::Long nDayY = nY+mnDaysOffY; + nDayIndex = static_cast<sal_uInt16>(aDate.GetDayOfWeek()); + nDayIndex = (nDayIndex+(7-static_cast<sal_uInt16>(ImplGetWeekStart()))) % 7; + for ( sal_uInt16 nDay = 1; nDay <= nDaysInMonth; nDay++ ) + { + if ( nDay == rDate.GetDay() ) + { + aRect.SetLeft( nDayX + (nDayIndex*mnDayWidth) ); + aRect.SetTop( nDayY ); + aRect.SetRight( aRect.Left()+mnDayWidth ); + aRect.SetBottom( aRect.Top()+mnDayHeight ); + break; + } + if ( nDayIndex == 6 ) + { + nDayIndex = 0; + nDayY += mnDayHeight; + } + else + nDayIndex++; + } + } + + aDate.AddDays( nDaysInMonth ); + nX += mnMonthWidth; + } + + nY += mnMonthHeight; + } + + return aRect; +} + +void Calendar::EndSelection() +{ + if ( mbDrag || mbSpinDown ) + { + ReleaseMouse(); + + mbDrag = false; + mbSpinDown = false; + mbPrevIn = false; + mbNextIn = false; + } +} + +Size Calendar::CalcWindowSizePixel() const +{ + Size aSize; + tools::Long n99TextWidth = GetTextWidth( "99" ); + tools::Long nTextHeight = GetTextHeight(); + + aSize.AdjustWidth((n99TextWidth+DAY_OFFX)*7); + aSize.AdjustWidth(MONTH_BORDERX*2 ); + + aSize.setHeight( nTextHeight + TITLE_OFFY + (TITLE_BORDERY*2) ); + aSize.AdjustHeight(nTextHeight + WEEKDAY_OFFY ); + aSize.AdjustHeight((nTextHeight+DAY_OFFY)*6); + aSize.AdjustHeight(MONTH_OFFY ); + + return aSize; +} + +Size Calendar::GetOptimalSize() const +{ + return CalcWindowSizePixel(); +} + +namespace +{ + class ImplCFieldFloat final + { + private: + std::unique_ptr<weld::Builder> mxBuilder; + std::unique_ptr<weld::Container> mxContainer; + std::unique_ptr<weld::Calendar> mxCalendar; + std::unique_ptr<weld::Button> mxTodayBtn; + std::unique_ptr<weld::Button> mxNoneBtn; + + public: + ImplCFieldFloat(vcl::Window* pContainer) + : mxBuilder(Application::CreateInterimBuilder(pContainer, "svt/ui/calendar.ui", false)) + , mxContainer(mxBuilder->weld_container("Calendar")) + , mxCalendar(mxBuilder->weld_calendar("date")) + , mxTodayBtn(mxBuilder->weld_button("today")) + , mxNoneBtn(mxBuilder->weld_button("none")) + { + } + + weld::Calendar* GetCalendar() { return mxCalendar.get(); } + weld::Button* EnableTodayBtn(bool bEnable); + weld::Button* EnableNoneBtn(bool bEnable); + + void GrabFocus() + { + mxCalendar->grab_focus(); + } + }; +} + +struct ImplCFieldFloatWin : public DropdownDockingWindow +{ + explicit ImplCFieldFloatWin(vcl::Window* pParent); + virtual void dispose() override; + virtual ~ImplCFieldFloatWin() override; + virtual void GetFocus() override; + + std::unique_ptr<ImplCFieldFloat> mxWidget; +}; + +ImplCFieldFloatWin::ImplCFieldFloatWin(vcl::Window* pParent) + : DropdownDockingWindow(pParent) +{ + setDeferredProperties(); + mxWidget.reset(new ImplCFieldFloat(m_xBox.get())); +} + +ImplCFieldFloatWin::~ImplCFieldFloatWin() +{ + disposeOnce(); +} + +void ImplCFieldFloatWin::dispose() +{ + mxWidget.reset(); + DropdownDockingWindow::dispose(); +} + +void ImplCFieldFloatWin::GetFocus() +{ + DropdownDockingWindow::GetFocus(); + if (!mxWidget) + return; + mxWidget->GrabFocus(); +} + +weld::Button* ImplCFieldFloat::EnableTodayBtn(bool bEnable) +{ + mxTodayBtn->set_visible(bEnable); + return bEnable ? mxTodayBtn.get() : nullptr; +} + +weld::Button* ImplCFieldFloat::EnableNoneBtn(bool bEnable) +{ + mxNoneBtn->set_visible(bEnable); + return bEnable ? mxNoneBtn.get() : nullptr; +} + +CalendarField::CalendarField(vcl::Window* pParent, WinBits nWinStyle) + : DateField(pParent, nWinStyle) + , mpFloatWin(nullptr) + , mpTodayBtn(nullptr) + , mpNoneBtn(nullptr) + , mbToday(false) + , mbNone(false) +{ +} + +CalendarField::~CalendarField() +{ + disposeOnce(); +} + +void CalendarField::dispose() +{ + mpTodayBtn = nullptr; + mpNoneBtn = nullptr; + mpFloatWin.disposeAndClear(); + DateField::dispose(); +} + +IMPL_LINK(CalendarField, ImplSelectHdl, weld::Calendar&, rCalendar, void) +{ + Date aNewDate = rCalendar.get_date(); + + vcl::Window::GetDockingManager()->EndPopupMode(mpFloatWin); + mpFloatWin->EnableDocking(false); + EndDropDown(); + GrabFocus(); + if ( IsEmptyDate() || ( aNewDate != GetDate() ) ) + { + SetDate( aNewDate ); + SetModifyFlag(); + Modify(); + } +} + +IMPL_LINK(CalendarField, ImplClickHdl, weld::Button&, rBtn, void) +{ + vcl::Window::GetDockingManager()->EndPopupMode(mpFloatWin); + mpFloatWin->EnableDocking(false); + EndDropDown(); + GrabFocus(); + + if (&rBtn == mpTodayBtn) + { + Date aToday( Date::SYSTEM ); + if ( (aToday != GetDate()) || IsEmptyDate() ) + { + SetDate( aToday ); + SetModifyFlag(); + Modify(); + } + } + else if (&rBtn == mpNoneBtn) + { + if ( !IsEmptyDate() ) + { + SetEmptyDate(); + SetModifyFlag(); + Modify(); + } + } +} + +IMPL_LINK_NOARG(CalendarField, ImplPopupModeEndHdl, FloatingWindow*, void) +{ + EndDropDown(); + GrabFocus(); +} + +bool CalendarField::ShowDropDown( bool bShow ) +{ + if ( bShow ) + { + if ( !mpFloatWin ) + mpFloatWin = VclPtr<ImplCFieldFloatWin>::Create( this ); + + Date aDate = GetDate(); + if ( IsEmptyDate() || !aDate.IsValidAndGregorian() ) + { + aDate = Date( Date::SYSTEM ); + } + weld::Calendar* pCalendar = mpFloatWin->mxWidget->GetCalendar(); + pCalendar->set_date( aDate ); + pCalendar->connect_activated(LINK(this, CalendarField, ImplSelectHdl)); + mpTodayBtn = mpFloatWin->mxWidget->EnableTodayBtn(mbToday); + mpNoneBtn = mpFloatWin->mxWidget->EnableNoneBtn(mbNone); + if (mpTodayBtn) + mpTodayBtn->connect_clicked( LINK( this, CalendarField, ImplClickHdl ) ); + if (mpNoneBtn) + mpNoneBtn->connect_clicked( LINK( this, CalendarField, ImplClickHdl ) ); + Point aPos(GetParent()->OutputToScreenPixel(GetPosPixel())); + tools::Rectangle aRect(aPos, GetSizePixel()); + aRect.AdjustBottom( -1 ); + DockingManager* pDockingManager = vcl::Window::GetDockingManager(); + mpFloatWin->EnableDocking(true); + pDockingManager->SetPopupModeEndHdl(mpFloatWin, LINK(this, CalendarField, ImplPopupModeEndHdl)); + pDockingManager->StartPopupMode(mpFloatWin, aRect, FloatWinPopupFlags::Down | FloatWinPopupFlags::GrabFocus); + } + else + { + vcl::Window::GetDockingManager()->EndPopupMode(mpFloatWin); + mpFloatWin->EnableDocking(false); + EndDropDown(); + } + return true; +} + +void CalendarField::StateChanged( StateChangedType nStateChange ) +{ + DateField::StateChanged( nStateChange ); + + if ( ( nStateChange == StateChangedType::Style ) && GetSubEdit() ) + { + WinBits nAllAlignmentBits = ( WB_LEFT | WB_CENTER | WB_RIGHT | WB_TOP | WB_VCENTER | WB_BOTTOM ); + WinBits nMyAlignment = GetStyle() & nAllAlignmentBits; + GetSubEdit()->SetStyle( ( GetSubEdit()->GetStyle() & ~nAllAlignmentBits ) | nMyAlignment ); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/combobox.cxx b/vcl/source/control/combobox.cxx new file mode 100644 index 000000000..15e088d7f --- /dev/null +++ b/vcl/source/control/combobox.cxx @@ -0,0 +1,1572 @@ +/* -*- 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 <vcl/toolkit/combobox.hxx> + +#include <set> + +#include <comphelper/string.hxx> +#include <vcl/toolkit/lstbox.hxx> +#include <vcl/builder.hxx> +#include <vcl/commandevent.hxx> +#include <vcl/event.hxx> +#include <vcl/settings.hxx> +#include <vcl/vclevent.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <sal/log.hxx> + +#include <listbox.hxx> +#include <comphelper/lok.hxx> +#include <tools/json_writer.hxx> +#include <o3tl/string_view.hxx> + +namespace { + +struct ComboBoxBounds +{ + Point aSubEditPos; + Size aSubEditSize; + + Point aButtonPos; + Size aButtonSize; +}; + +} + +struct ComboBox::Impl +{ + ComboBox & m_rThis; + VclPtr<Edit> m_pSubEdit; + VclPtr<ImplListBox> m_pImplLB; + VclPtr<ImplBtn> m_pBtn; + VclPtr<ImplListBoxFloatingWindow> m_pFloatWin; + sal_uInt16 m_nDDHeight; + sal_Unicode m_cMultiSep; + bool m_isDDAutoSize : 1; + bool m_isSyntheticModify : 1; + bool m_isKeyBoardModify : 1; + bool m_isMatchCase : 1; + sal_Int32 m_nMaxWidthChars; + sal_Int32 m_nWidthInChars; + Link<ComboBox&,void> m_SelectHdl; + + explicit Impl(ComboBox & rThis) + : m_rThis(rThis) + , m_nDDHeight(0) + , m_cMultiSep(0) + , m_isDDAutoSize(false) + , m_isSyntheticModify(false) + , m_isKeyBoardModify(false) + , m_isMatchCase(false) + , m_nMaxWidthChars(0) + , m_nWidthInChars(-1) + { + } + + void ImplInitComboBoxData(); + void ImplUpdateFloatSelection(); + ComboBoxBounds calcComboBoxDropDownComponentBounds( + const Size &rOutSize, const Size &rBorderOutSize) const; + + DECL_LINK( ImplSelectHdl, LinkParamNone*, void ); + DECL_LINK( ImplCancelHdl, LinkParamNone*, void ); + DECL_LINK( ImplDoubleClickHdl, ImplListBoxWindow*, void ); + DECL_LINK( ImplClickBtnHdl, void*, void ); + DECL_LINK( ImplPopupModeEndHdl, FloatingWindow*, void ); + DECL_LINK( ImplSelectionChangedHdl, sal_Int32, void ); + DECL_LINK( ImplAutocompleteHdl, Edit&, void ); + DECL_LINK( ImplListItemSelectHdl , LinkParamNone*, void ); +}; + + +static void lcl_GetSelectedEntries( ::std::set< sal_Int32 >& rSelectedPos, std::u16string_view rText, sal_Unicode cTokenSep, const ImplEntryList& rEntryList ) +{ + if (rText.empty()) + return; + + sal_Int32 nIdx{0}; + do { + const sal_Int32 nPos = rEntryList.FindEntry(comphelper::string::strip(o3tl::getToken(rText, 0, cTokenSep, nIdx), ' ')); + if ( nPos != LISTBOX_ENTRY_NOTFOUND ) + rSelectedPos.insert( nPos ); + } while (nIdx>=0); +} + +ComboBox::ComboBox(vcl::Window *const pParent, WinBits const nStyle) + : Edit( WindowType::COMBOBOX ) + , m_pImpl(new Impl(*this)) +{ + m_pImpl->ImplInitComboBoxData(); + ImplInit( pParent, nStyle ); + SetWidthInChars(-1); +} + +ComboBox::~ComboBox() +{ + disposeOnce(); +} + +void ComboBox::dispose() +{ + m_pImpl->m_pSubEdit.disposeAndClear(); + + VclPtr< ImplListBox > pImplLB = m_pImpl->m_pImplLB; + m_pImpl->m_pImplLB.clear(); + pImplLB.disposeAndClear(); + + m_pImpl->m_pFloatWin.disposeAndClear(); + m_pImpl->m_pBtn.disposeAndClear(); + Edit::dispose(); +} + +void ComboBox::Impl::ImplInitComboBoxData() +{ + m_pSubEdit.disposeAndClear(); + m_pBtn = nullptr; + m_pImplLB = nullptr; + m_pFloatWin = nullptr; + + m_nDDHeight = 0; + m_isDDAutoSize = true; + m_isSyntheticModify = false; + m_isKeyBoardModify = false; + m_isMatchCase = false; + m_cMultiSep = ';'; + m_nMaxWidthChars = -1; + m_nWidthInChars = -1; +} + +void ComboBox::ImplCalcEditHeight() +{ + sal_Int32 nLeft, nTop, nRight, nBottom; + GetBorder( nLeft, nTop, nRight, nBottom ); + m_pImpl->m_nDDHeight = static_cast<sal_uInt16>(m_pImpl->m_pSubEdit->GetTextHeight() + nTop + nBottom + 4); + if ( !IsDropDownBox() ) + m_pImpl->m_nDDHeight += 4; + + tools::Rectangle aCtrlRegion( Point( 0, 0 ), Size( 10, 10 ) ); + tools::Rectangle aBoundRegion, aContentRegion; + ImplControlValue aControlValue; + ControlType aType = IsDropDownBox() ? ControlType::Combobox : ControlType::Editbox; + if( GetNativeControlRegion( aType, ControlPart::Entire, + aCtrlRegion, + ControlState::ENABLED, + aControlValue, + aBoundRegion, aContentRegion ) ) + { + const tools::Long nNCHeight = aBoundRegion.GetHeight(); + if (m_pImpl->m_nDDHeight < nNCHeight) + m_pImpl->m_nDDHeight = sal::static_int_cast<sal_uInt16>(nNCHeight); + } +} + +void ComboBox::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + bool bNoBorder = ( nStyle & WB_NOBORDER ) != 0; + if ( !(nStyle & WB_DROPDOWN) ) + { + nStyle &= ~WB_BORDER; + nStyle |= WB_NOBORDER; + } + else + { + if ( !bNoBorder ) + nStyle |= WB_BORDER; + } + + Edit::ImplInit( pParent, nStyle ); + SetBackground(); + + // DropDown ? + WinBits nEditStyle = nStyle & ( WB_LEFT | WB_RIGHT | WB_CENTER ); + WinBits nListStyle = nStyle; + if( nStyle & WB_DROPDOWN ) + { + m_pImpl->m_pFloatWin = VclPtr<ImplListBoxFloatingWindow>::Create( this ); + if (!IsNativeControlSupported(ControlType::Pushbutton, ControlPart::Focus)) + m_pImpl->m_pFloatWin->RequestDoubleBuffering(true); + m_pImpl->m_pFloatWin->SetAutoWidth( true ); + m_pImpl->m_pFloatWin->SetPopupModeEndHdl( LINK(m_pImpl.get(), ComboBox::Impl, ImplPopupModeEndHdl) ); + + m_pImpl->m_pBtn = VclPtr<ImplBtn>::Create( this, WB_NOLIGHTBORDER | WB_RECTSTYLE ); + ImplInitDropDownButton( m_pImpl->m_pBtn ); + m_pImpl->m_pBtn->SetMBDownHdl( LINK( m_pImpl.get(), ComboBox::Impl, ImplClickBtnHdl ) ); + m_pImpl->m_pBtn->Show(); + + nEditStyle |= WB_NOBORDER; + nListStyle &= ~WB_BORDER; + nListStyle |= WB_NOBORDER; + } + else + { + if ( !bNoBorder ) + { + nEditStyle |= WB_BORDER; + nListStyle &= ~WB_NOBORDER; + nListStyle |= WB_BORDER; + } + } + + m_pImpl->m_pSubEdit.set( VclPtr<Edit>::Create( this, nEditStyle ) ); + m_pImpl->m_pSubEdit->EnableRTL( false ); + SetSubEdit( m_pImpl->m_pSubEdit ); + m_pImpl->m_pSubEdit->SetPosPixel( Point() ); + EnableAutocomplete( true ); + m_pImpl->m_pSubEdit->Show(); + + vcl::Window* pLBParent = this; + if (m_pImpl->m_pFloatWin) + pLBParent = m_pImpl->m_pFloatWin; + m_pImpl->m_pImplLB = VclPtr<ImplListBox>::Create( pLBParent, nListStyle|WB_SIMPLEMODE|WB_AUTOHSCROLL ); + m_pImpl->m_pImplLB->SetPosPixel( Point() ); + m_pImpl->m_pImplLB->SetSelectHdl( LINK(m_pImpl.get(), ComboBox::Impl, ImplSelectHdl) ); + m_pImpl->m_pImplLB->SetCancelHdl( LINK(m_pImpl.get(), ComboBox::Impl, ImplCancelHdl) ); + m_pImpl->m_pImplLB->SetDoubleClickHdl( LINK(m_pImpl.get(), ComboBox::Impl, ImplDoubleClickHdl) ); + m_pImpl->m_pImplLB->SetSelectionChangedHdl( LINK(m_pImpl.get(), ComboBox::Impl, ImplSelectionChangedHdl) ); + m_pImpl->m_pImplLB->SetListItemSelectHdl( LINK(m_pImpl.get(), ComboBox::Impl, ImplListItemSelectHdl) ); + m_pImpl->m_pImplLB->Show(); + + if (m_pImpl->m_pFloatWin) + m_pImpl->m_pFloatWin->SetImplListBox( m_pImpl->m_pImplLB ); + else + GetMainWindow()->AllowGrabFocus( true ); + + ImplCalcEditHeight(); + + SetCompoundControl( true ); +} + +WinBits ComboBox::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + return nStyle; +} + +void ComboBox::EnableAutocomplete( bool bEnable, bool bMatchCase ) +{ + m_pImpl->m_isMatchCase = bMatchCase; + + if ( bEnable ) + m_pImpl->m_pSubEdit->SetAutocompleteHdl( LINK(m_pImpl.get(), ComboBox::Impl, ImplAutocompleteHdl) ); + else + m_pImpl->m_pSubEdit->SetAutocompleteHdl( Link<Edit&,void>() ); +} + +bool ComboBox::IsAutocompleteEnabled() const +{ + return m_pImpl->m_pSubEdit->GetAutocompleteHdl().IsSet(); +} + +IMPL_LINK_NOARG(ComboBox::Impl, ImplClickBtnHdl, void*, void) +{ + m_rThis.CallEventListeners( VclEventId::DropdownPreOpen ); + m_pSubEdit->GrabFocus(); + if (!m_pImplLB->GetEntryList().GetMRUCount()) + ImplUpdateFloatSelection(); + else + m_pImplLB->SelectEntry( 0 , true ); + m_pBtn->SetPressed( true ); + m_rThis.SetSelection( Selection( 0, SELECTION_MAX ) ); + m_pFloatWin->StartFloat( true ); + m_rThis.CallEventListeners( VclEventId::DropdownOpen ); + + m_rThis.ImplClearLayoutData(); + if (m_pImplLB) + m_pImplLB->GetMainWindow()->ImplClearLayoutData(); +} + +IMPL_LINK_NOARG(ComboBox::Impl, ImplPopupModeEndHdl, FloatingWindow*, void) +{ + if (m_pFloatWin->IsPopupModeCanceled()) + { + if (!m_pImplLB->GetEntryList().IsEntryPosSelected( + m_pFloatWin->GetPopupModeStartSaveSelection())) + { + m_pImplLB->SelectEntry(m_pFloatWin->GetPopupModeStartSaveSelection(), true); + bool bTravelSelect = m_pImplLB->IsTravelSelect(); + m_pImplLB->SetTravelSelect( true ); + m_rThis.Select(); + m_pImplLB->SetTravelSelect( bTravelSelect ); + } + } + + m_rThis.ImplClearLayoutData(); + if (m_pImplLB) + m_pImplLB->GetMainWindow()->ImplClearLayoutData(); + + m_pBtn->SetPressed( false ); + m_rThis.CallEventListeners( VclEventId::DropdownClose ); +} + +IMPL_LINK(ComboBox::Impl, ImplAutocompleteHdl, Edit&, rEdit, void) +{ + Selection aSel = rEdit.GetSelection(); + + { + OUString aFullText = rEdit.GetText(); + OUString aStartText = aFullText.copy( 0, static_cast<sal_Int32>(aSel.Max()) ); + sal_Int32 nStart = m_pImplLB->GetCurrentPos(); + + if ( nStart == LISTBOX_ENTRY_NOTFOUND ) + nStart = 0; + + sal_Int32 nPos = LISTBOX_ENTRY_NOTFOUND; + if (!m_isMatchCase) + { + // Try match case insensitive from current position + nPos = m_pImplLB->GetEntryList().FindMatchingEntry(aStartText, nStart, true); + if ( nPos == LISTBOX_ENTRY_NOTFOUND ) + // Try match case insensitive, but from start + nPos = m_pImplLB->GetEntryList().FindMatchingEntry(aStartText, 0, true); + } + + if ( nPos == LISTBOX_ENTRY_NOTFOUND ) + // Try match full from current position + nPos = m_pImplLB->GetEntryList().FindMatchingEntry(aStartText, nStart, false); + if ( nPos == LISTBOX_ENTRY_NOTFOUND ) + // Match full, but from start + nPos = m_pImplLB->GetEntryList().FindMatchingEntry(aStartText, 0, false); + + if ( nPos != LISTBOX_ENTRY_NOTFOUND ) + { + OUString aText = m_pImplLB->GetEntryList().GetEntryText( nPos ); + Selection aSelection( aText.getLength(), aStartText.getLength() ); + rEdit.SetText( aText, aSelection ); + } + } +} + +IMPL_LINK_NOARG(ComboBox::Impl, ImplSelectHdl, LinkParamNone*, void) +{ + bool bPopup = m_rThis.IsInDropDown(); + bool bCallSelect = false; + if (m_pImplLB->IsSelectionChanged() || bPopup) + { + OUString aText; + if (m_rThis.IsMultiSelectionEnabled()) + { + aText = m_pSubEdit->GetText(); + + // remove all entries to which there is an entry, but which is not selected + sal_Int32 nIndex = 0; + while ( nIndex >= 0 ) + { + sal_Int32 nPrevIndex = nIndex; + std::u16string_view aToken = o3tl::getToken(aText, 0, m_cMultiSep, nIndex ); + sal_Int32 nTokenLen = aToken.size(); + aToken = comphelper::string::strip(aToken, ' '); + sal_Int32 nP = m_pImplLB->GetEntryList().FindEntry( aToken ); + if ((nP != LISTBOX_ENTRY_NOTFOUND) && (!m_pImplLB->GetEntryList().IsEntryPosSelected(nP))) + { + aText = aText.replaceAt( nPrevIndex, nTokenLen, u"" ); + nIndex = nIndex - nTokenLen; + sal_Int32 nSepCount=0; + if ((nPrevIndex+nSepCount < aText.getLength()) && (aText[nPrevIndex+nSepCount] == m_cMultiSep)) + { + nIndex--; + ++nSepCount; + } + aText = aText.replaceAt( nPrevIndex, nSepCount, u"" ); + } + aText = comphelper::string::strip(aText, ' '); + } + + // attach missing entries + ::std::set< sal_Int32 > aSelInText; + lcl_GetSelectedEntries( aSelInText, aText, m_cMultiSep, m_pImplLB->GetEntryList() ); + sal_Int32 nSelectedEntries = m_pImplLB->GetEntryList().GetSelectedEntryCount(); + for ( sal_Int32 n = 0; n < nSelectedEntries; n++ ) + { + sal_Int32 nP = m_pImplLB->GetEntryList().GetSelectedEntryPos( n ); + if ( !aSelInText.count( nP ) ) + { + if (!aText.isEmpty() && (aText[aText.getLength()-1] != m_cMultiSep)) + aText += OUStringChar(m_cMultiSep); + if ( !aText.isEmpty() ) + aText += " "; // slightly loosen + aText += m_pImplLB->GetEntryList().GetEntryText( nP ) + + OUStringChar(m_cMultiSep); + } + } + aText = comphelper::string::stripEnd( aText, m_cMultiSep ); + } + else + { + aText = m_pImplLB->GetEntryList().GetSelectedEntry( 0 ); + } + + m_pSubEdit->SetText( aText ); + + Selection aNewSelection( 0, aText.getLength() ); + if (m_rThis.IsMultiSelectionEnabled()) + aNewSelection.Min() = aText.getLength(); + m_pSubEdit->SetSelection( aNewSelection ); + + bCallSelect = true; + } + + // #84652# Call GrabFocus and EndPopupMode before calling Select/Modify, but after changing the text + bool bMenuSelect = bPopup && !m_pImplLB->IsTravelSelect() && (!m_rThis.IsMultiSelectionEnabled() || !m_pImplLB->GetSelectModifier()); + if (bMenuSelect) + { + m_pFloatWin->EndPopupMode(); + m_rThis.GrabFocus(); + } + + if ( bCallSelect ) + { + m_isKeyBoardModify = !bMenuSelect; + m_pSubEdit->SetModifyFlag(); + m_isSyntheticModify = true; + m_rThis.Modify(); + m_isSyntheticModify = false; + m_rThis.Select(); + m_isKeyBoardModify = false; + } +} + +bool ComboBox::IsSyntheticModify() const +{ + return m_pImpl->m_isSyntheticModify; +} + +bool ComboBox::IsModifyByKeyboard() const +{ + return m_pImpl->m_isKeyBoardModify; +} + +IMPL_LINK_NOARG( ComboBox::Impl, ImplListItemSelectHdl, LinkParamNone*, void ) +{ + m_rThis.CallEventListeners( VclEventId::DropdownSelect ); +} + +IMPL_LINK_NOARG(ComboBox::Impl, ImplCancelHdl, LinkParamNone*, void) +{ + if (m_rThis.IsInDropDown()) + m_pFloatWin->EndPopupMode(); +} + +IMPL_LINK( ComboBox::Impl, ImplSelectionChangedHdl, sal_Int32, nChanged, void ) +{ + if (!m_pImplLB->IsTrackingSelect()) + { + if (!m_pSubEdit->IsReadOnly() && m_pImplLB->GetEntryList().IsEntryPosSelected(nChanged)) + m_pSubEdit->SetText(m_pImplLB->GetEntryList().GetEntryText(nChanged)); + } +} + +IMPL_LINK_NOARG(ComboBox::Impl, ImplDoubleClickHdl, ImplListBoxWindow*, void) +{ + m_rThis.DoubleClick(); +} + +void ComboBox::ToggleDropDown() +{ + if( !IsDropDownBox() ) + return; + + if (m_pImpl->m_pFloatWin->IsInPopupMode()) + m_pImpl->m_pFloatWin->EndPopupMode(); + else + { + m_pImpl->m_pSubEdit->GrabFocus(); + if (!m_pImpl->m_pImplLB->GetEntryList().GetMRUCount()) + m_pImpl->ImplUpdateFloatSelection(); + else + m_pImpl->m_pImplLB->SelectEntry( 0 , true ); + CallEventListeners( VclEventId::DropdownPreOpen ); + m_pImpl->m_pBtn->SetPressed( true ); + SetSelection( Selection( 0, SELECTION_MAX ) ); + m_pImpl->m_pFloatWin->StartFloat( true ); + CallEventListeners( VclEventId::DropdownOpen ); + } +} + +void ComboBox::Select() +{ + ImplCallEventListenersAndHandler( VclEventId::ComboboxSelect, [this] () { m_pImpl->m_SelectHdl.Call(*this); } ); +} + +void ComboBox::DoubleClick() +{ + ImplCallEventListenersAndHandler( VclEventId::ComboboxDoubleClick, [] () {} ); +} + +bool ComboBox::IsAutoSizeEnabled() const { return m_pImpl->m_isDDAutoSize; } + +void ComboBox::EnableAutoSize( bool bAuto ) +{ + m_pImpl->m_isDDAutoSize = bAuto; + if (m_pImpl->m_pFloatWin) + { + if (bAuto && !m_pImpl->m_pFloatWin->GetDropDownLineCount()) + { + // Adapt to GetListBoxMaximumLineCount here; was on fixed number of five before + AdaptDropDownLineCountToMaximum(); + } + else if ( !bAuto ) + { + m_pImpl->m_pFloatWin->SetDropDownLineCount( 0 ); + } + } +} + +void ComboBox::SetDropDownLineCount( sal_uInt16 nLines ) +{ + if (m_pImpl->m_pFloatWin) + m_pImpl->m_pFloatWin->SetDropDownLineCount( nLines ); +} + +void ComboBox::AdaptDropDownLineCountToMaximum() +{ + // Adapt to maximum allowed number. + // Limit for LOK as we can't render outside of the dialog canvas. + if (comphelper::LibreOfficeKit::isActive()) + SetDropDownLineCount(11); + else + SetDropDownLineCount(GetSettings().GetStyleSettings().GetListBoxMaximumLineCount()); +} + +sal_uInt16 ComboBox::GetDropDownLineCount() const +{ + sal_uInt16 nLines = 0; + if (m_pImpl->m_pFloatWin) + nLines = m_pImpl->m_pFloatWin->GetDropDownLineCount(); + return nLines; +} + +void ComboBox::setPosSizePixel( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, + PosSizeFlags nFlags ) +{ + if( IsDropDownBox() && ( nFlags & PosSizeFlags::Size ) ) + { + Size aPrefSz = m_pImpl->m_pFloatWin->GetPrefSize(); + if ((nFlags & PosSizeFlags::Height) && (nHeight >= 2*m_pImpl->m_nDDHeight)) + aPrefSz.setHeight( nHeight-m_pImpl->m_nDDHeight ); + if ( nFlags & PosSizeFlags::Width ) + aPrefSz.setWidth( nWidth ); + m_pImpl->m_pFloatWin->SetPrefSize( aPrefSz ); + + if (IsAutoSizeEnabled()) + nHeight = m_pImpl->m_nDDHeight; + } + + Edit::setPosSizePixel( nX, nY, nWidth, nHeight, nFlags ); +} + +void ComboBox::Resize() +{ + Control::Resize(); + + if (m_pImpl->m_pSubEdit) + { + Size aOutSz = GetOutputSizePixel(); + if( IsDropDownBox() ) + { + ComboBoxBounds aBounds(m_pImpl->calcComboBoxDropDownComponentBounds(aOutSz, + GetWindow(GetWindowType::Border)->GetOutputSizePixel())); + m_pImpl->m_pSubEdit->SetPosSizePixel(aBounds.aSubEditPos, aBounds.aSubEditSize); + m_pImpl->m_pBtn->SetPosSizePixel(aBounds.aButtonPos, aBounds.aButtonSize); + } + else + { + m_pImpl->m_pSubEdit->SetSizePixel(Size(aOutSz.Width(), m_pImpl->m_nDDHeight)); + m_pImpl->m_pImplLB->setPosSizePixel(0, m_pImpl->m_nDDHeight, + aOutSz.Width(), aOutSz.Height() - m_pImpl->m_nDDHeight); + if ( !GetText().isEmpty() ) + m_pImpl->ImplUpdateFloatSelection(); + } + } + + // adjust the size of the FloatingWindow even when invisible + // as KEY_PGUP/DOWN is being processed... + if (m_pImpl->m_pFloatWin) + m_pImpl->m_pFloatWin->SetSizePixel(m_pImpl->m_pFloatWin->CalcFloatSize()); +} + +bool ComboBox::IsDropDownBox() const { return m_pImpl->m_pFloatWin != nullptr; } + +void ComboBox::FillLayoutData() const +{ + mxLayoutData.emplace(); + AppendLayoutData( *m_pImpl->m_pSubEdit ); + m_pImpl->m_pSubEdit->SetLayoutDataParent( this ); + ImplListBoxWindow* rMainWindow = GetMainWindow(); + if (m_pImpl->m_pFloatWin) + { + // dropdown mode + if (m_pImpl->m_pFloatWin->IsReallyVisible()) + { + AppendLayoutData( *rMainWindow ); + rMainWindow->SetLayoutDataParent( this ); + } + } + else + { + AppendLayoutData( *rMainWindow ); + rMainWindow->SetLayoutDataParent( this ); + } +} + +void ComboBox::StateChanged( StateChangedType nType ) +{ + Edit::StateChanged( nType ); + + if ( nType == StateChangedType::ReadOnly ) + { + m_pImpl->m_pImplLB->SetReadOnly( IsReadOnly() ); + if (m_pImpl->m_pBtn) + m_pImpl->m_pBtn->Enable( IsEnabled() && !IsReadOnly() ); + } + else if ( nType == StateChangedType::Enable ) + { + m_pImpl->m_pSubEdit->Enable( IsEnabled() ); + m_pImpl->m_pImplLB->Enable( IsEnabled() && !IsReadOnly() ); + if (m_pImpl->m_pBtn) + m_pImpl->m_pBtn->Enable( IsEnabled() && !IsReadOnly() ); + Invalidate(); + } + else if( nType == StateChangedType::UpdateMode ) + { + m_pImpl->m_pImplLB->SetUpdateMode( IsUpdateMode() ); + } + else if ( nType == StateChangedType::Zoom ) + { + m_pImpl->m_pImplLB->SetZoom( GetZoom() ); + m_pImpl->m_pSubEdit->SetZoom( GetZoom() ); + ImplCalcEditHeight(); + Resize(); + } + else if ( nType == StateChangedType::ControlFont ) + { + m_pImpl->m_pImplLB->SetControlFont( GetControlFont() ); + m_pImpl->m_pSubEdit->SetControlFont( GetControlFont() ); + ImplCalcEditHeight(); + Resize(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + m_pImpl->m_pImplLB->SetControlForeground( GetControlForeground() ); + m_pImpl->m_pSubEdit->SetControlForeground( GetControlForeground() ); + } + else if ( nType == StateChangedType::ControlBackground ) + { + m_pImpl->m_pImplLB->SetControlBackground( GetControlBackground() ); + m_pImpl->m_pSubEdit->SetControlBackground( GetControlBackground() ); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetStyle() ) ); + GetMainWindow()->EnableSort( ( GetStyle() & WB_SORT ) != 0 ); + } + else if( nType == StateChangedType::Mirroring ) + { + if (m_pImpl->m_pBtn) + { + m_pImpl->m_pBtn->EnableRTL( IsRTLEnabled() ); + ImplInitDropDownButton( m_pImpl->m_pBtn ); + } + m_pImpl->m_pSubEdit->CompatStateChanged( StateChangedType::Mirroring ); + m_pImpl->m_pImplLB->EnableRTL( IsRTLEnabled() ); + Resize(); + } +} + +void ComboBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( !((rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE))) ) + return; + + if (m_pImpl->m_pBtn) + { + m_pImpl->m_pBtn->GetOutDev()->SetSettings( GetSettings() ); + ImplInitDropDownButton( m_pImpl->m_pBtn ); + } + Resize(); + m_pImpl->m_pImplLB->Resize(); // not called by ComboBox::Resize() if ImplLB is unchanged + + SetBackground(); // due to a hack in Window::UpdateSettings the background must be reset + // otherwise it will overpaint NWF drawn comboboxes +} + +bool ComboBox::EventNotify( NotifyEvent& rNEvt ) +{ + bool bDone = false; + if ((rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) + && (rNEvt.GetWindow() == m_pImpl->m_pSubEdit) + && !IsReadOnly()) + { + KeyEvent aKeyEvt = *rNEvt.GetKeyEvent(); + sal_uInt16 nKeyCode = aKeyEvt.GetKeyCode().GetCode(); + switch( nKeyCode ) + { + case KEY_UP: + case KEY_DOWN: + case KEY_PAGEUP: + case KEY_PAGEDOWN: + { + m_pImpl->ImplUpdateFloatSelection(); + if ((nKeyCode == KEY_DOWN) && m_pImpl->m_pFloatWin + && !m_pImpl->m_pFloatWin->IsInPopupMode() + && aKeyEvt.GetKeyCode().IsMod2()) + { + CallEventListeners( VclEventId::DropdownPreOpen ); + m_pImpl->m_pBtn->SetPressed( true ); + if (m_pImpl->m_pImplLB->GetEntryList().GetMRUCount()) + m_pImpl->m_pImplLB->SelectEntry( 0 , true ); + SetSelection( Selection( 0, SELECTION_MAX ) ); + m_pImpl->m_pFloatWin->StartFloat( false ); + CallEventListeners( VclEventId::DropdownOpen ); + bDone = true; + } + else if ((nKeyCode == KEY_UP) && m_pImpl->m_pFloatWin + && m_pImpl->m_pFloatWin->IsInPopupMode() + && aKeyEvt.GetKeyCode().IsMod2()) + { + m_pImpl->m_pFloatWin->EndPopupMode(); + bDone = true; + } + else + { + bDone = m_pImpl->m_pImplLB->ProcessKeyInput( aKeyEvt ); + } + } + break; + + case KEY_RETURN: + { + if ((rNEvt.GetWindow() == m_pImpl->m_pSubEdit) && IsInDropDown()) + { + m_pImpl->m_pImplLB->ProcessKeyInput( aKeyEvt ); + bDone = true; + } + } + break; + } + } + else if ((rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS) && m_pImpl->m_pFloatWin) + { + if (m_pImpl->m_pFloatWin->HasChildPathFocus()) + m_pImpl->m_pSubEdit->GrabFocus(); + else if (m_pImpl->m_pFloatWin->IsInPopupMode() && !HasChildPathFocus(true)) + m_pImpl->m_pFloatWin->EndPopupMode(); + } + else if( (rNEvt.GetType() == MouseNotifyEvent::COMMAND) && + (rNEvt.GetCommandEvent()->GetCommand() == CommandEventId::Wheel) && + (rNEvt.GetWindow() == m_pImpl->m_pSubEdit) ) + { + MouseWheelBehaviour nWheelBehavior( GetSettings().GetMouseSettings().GetWheelBehavior() ); + if ( ( nWheelBehavior == MouseWheelBehaviour::ALWAYS ) + || ( ( nWheelBehavior == MouseWheelBehaviour::FocusOnly ) + && HasChildPathFocus() + ) + ) + { + bDone = m_pImpl->m_pImplLB->HandleWheelAsCursorTravel(*rNEvt.GetCommandEvent(), *this); + } + else + { + bDone = false; // don't eat this event, let the default handling happen (i.e. scroll the context) + } + } + else if ((rNEvt.GetType() == MouseNotifyEvent::MOUSEBUTTONDOWN) + && (rNEvt.GetWindow() == GetMainWindow())) + { + m_pImpl->m_pSubEdit->GrabFocus(); + } + + return bDone || Edit::EventNotify( rNEvt ); +} + +void ComboBox::SetText( const OUString& rStr ) +{ + CallEventListeners( VclEventId::ComboboxSetText ); + + Edit::SetText( rStr ); + m_pImpl->ImplUpdateFloatSelection(); +} + +void ComboBox::SetText( const OUString& rStr, const Selection& rNewSelection ) +{ + CallEventListeners( VclEventId::ComboboxSetText ); + + Edit::SetText( rStr, rNewSelection ); + m_pImpl->ImplUpdateFloatSelection(); +} + +void ComboBox::Modify() +{ + if (!m_pImpl->m_isSyntheticModify) + m_pImpl->ImplUpdateFloatSelection(); + + Edit::Modify(); +} + +void ComboBox::Impl::ImplUpdateFloatSelection() +{ + if (!m_pImplLB || !m_pSubEdit) + return; + + // move text in the ListBox into the visible region + m_pImplLB->SetCallSelectionChangedHdl( false ); + if (!m_rThis.IsMultiSelectionEnabled()) + { + OUString aSearchStr( m_pSubEdit->GetText() ); + sal_Int32 nSelect = LISTBOX_ENTRY_NOTFOUND; + bool bSelect = true; + + if (m_pImplLB->GetCurrentPos() != LISTBOX_ENTRY_NOTFOUND) + { + OUString aCurrent = m_pImplLB->GetEntryList().GetEntryText( + m_pImplLB->GetCurrentPos()); + if ( aCurrent == aSearchStr ) + nSelect = m_pImplLB->GetCurrentPos(); + } + + if ( nSelect == LISTBOX_ENTRY_NOTFOUND ) + nSelect = m_pImplLB->GetEntryList().FindEntry( aSearchStr ); + if ( nSelect == LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = m_pImplLB->GetEntryList().FindMatchingEntry( aSearchStr, 0, true ); + bSelect = false; + } + + if( nSelect != LISTBOX_ENTRY_NOTFOUND ) + { + if (!m_pImplLB->IsVisible(nSelect)) + m_pImplLB->ShowProminentEntry( nSelect ); + m_pImplLB->SelectEntry( nSelect, bSelect ); + } + else + { + nSelect = m_pImplLB->GetEntryList().GetSelectedEntryPos( 0 ); + if( nSelect != LISTBOX_ENTRY_NOTFOUND ) + m_pImplLB->SelectEntry( nSelect, false ); + m_pImplLB->ResetCurrentPos(); + } + } + else + { + ::std::set< sal_Int32 > aSelInText; + lcl_GetSelectedEntries(aSelInText, m_pSubEdit->GetText(), m_cMultiSep, m_pImplLB->GetEntryList()); + for (sal_Int32 n = 0; n < m_pImplLB->GetEntryList().GetEntryCount(); n++) + m_pImplLB->SelectEntry( n, aSelInText.count( n ) != 0 ); + } + m_pImplLB->SetCallSelectionChangedHdl( true ); +} + +sal_Int32 ComboBox::InsertEntry(const OUString& rStr, sal_Int32 const nPos) +{ + assert(nPos >= 0 && COMBOBOX_MAX_ENTRIES > m_pImpl->m_pImplLB->GetEntryList().GetEntryCount()); + + sal_Int32 nRealPos; + if (nPos == COMBOBOX_APPEND) + nRealPos = nPos; + else + { + const sal_Int32 nMRUCount = m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); + assert(nPos <= COMBOBOX_MAX_ENTRIES - nMRUCount); + nRealPos = nPos + nMRUCount; + } + + nRealPos = m_pImpl->m_pImplLB->InsertEntry( nRealPos, rStr ); + nRealPos -= m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); + CallEventListeners( VclEventId::ComboboxItemAdded, reinterpret_cast<void*>(nRealPos) ); + return nRealPos; +} + +sal_Int32 ComboBox::InsertEntryWithImage( + const OUString& rStr, const Image& rImage, sal_Int32 const nPos) +{ + assert(nPos >= 0 && COMBOBOX_MAX_ENTRIES > m_pImpl->m_pImplLB->GetEntryList().GetEntryCount()); + + sal_Int32 nRealPos; + if (nPos == COMBOBOX_APPEND) + nRealPos = nPos; + else + { + const sal_Int32 nMRUCount = m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); + assert(nPos <= COMBOBOX_MAX_ENTRIES - nMRUCount); + nRealPos = nPos + nMRUCount; + } + + nRealPos = m_pImpl->m_pImplLB->InsertEntry( nRealPos, rStr, rImage ); + nRealPos -= m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); + CallEventListeners( VclEventId::ComboboxItemAdded, reinterpret_cast<void*>(nRealPos) ); + return nRealPos; +} + +void ComboBox::RemoveEntryAt(sal_Int32 const nPos) +{ + const sal_Int32 nMRUCount = m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); + assert(nPos >= 0 && nPos <= COMBOBOX_MAX_ENTRIES - nMRUCount); + m_pImpl->m_pImplLB->RemoveEntry( nPos + nMRUCount ); + CallEventListeners( VclEventId::ComboboxItemRemoved, reinterpret_cast<void*>(nPos) ); +} + +void ComboBox::Clear() +{ + if (!m_pImpl->m_pImplLB) + return; + m_pImpl->m_pImplLB->Clear(); + CallEventListeners( VclEventId::ComboboxItemRemoved, reinterpret_cast<void*>(-1) ); +} + +Image ComboBox::GetEntryImage( sal_Int32 nPos ) const +{ + if (m_pImpl->m_pImplLB->GetEntryList().HasEntryImage(nPos)) + return m_pImpl->m_pImplLB->GetEntryList().GetEntryImage( nPos ); + return Image(); +} + +sal_Int32 ComboBox::GetEntryPos( std::u16string_view rStr ) const +{ + sal_Int32 nPos = m_pImpl->m_pImplLB->GetEntryList().FindEntry( rStr ); + if ( nPos != LISTBOX_ENTRY_NOTFOUND ) + nPos -= m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); + return nPos; +} + +OUString ComboBox::GetEntry( sal_Int32 nPos ) const +{ + const sal_Int32 nMRUCount = m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); + if (nPos < 0 || nPos > COMBOBOX_MAX_ENTRIES - nMRUCount) + return OUString(); + + return m_pImpl->m_pImplLB->GetEntryList().GetEntryText( nPos + nMRUCount ); +} + +sal_Int32 ComboBox::GetEntryCount() const +{ + if (!m_pImpl->m_pImplLB) + return 0; + return m_pImpl->m_pImplLB->GetEntryList().GetEntryCount() - m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(); +} + +bool ComboBox::IsTravelSelect() const +{ + return m_pImpl->m_pImplLB->IsTravelSelect(); +} + +bool ComboBox::IsInDropDown() const +{ + // when the dropdown is dismissed, first mbInPopupMode is set to false, and on the next event iteration then + // mbPopupMode is set to false + return m_pImpl->m_pFloatWin && m_pImpl->m_pFloatWin->IsInPopupMode() && m_pImpl->m_pFloatWin->ImplIsInPrivatePopupMode(); +} + +bool ComboBox::IsMultiSelectionEnabled() const +{ + return m_pImpl->m_pImplLB->IsMultiSelectionEnabled(); +} + +void ComboBox::SetSelectHdl(const Link<ComboBox&,void>& rLink) { m_pImpl->m_SelectHdl = rLink; } + +void ComboBox::SetEntryActivateHdl(const Link<Edit&,bool>& rLink) +{ + if (!m_pImpl->m_pSubEdit) + return; + m_pImpl->m_pSubEdit->SetActivateHdl(rLink); +} + +Size ComboBox::GetOptimalSize() const +{ + return CalcMinimumSize(); +} + +tools::Long ComboBox::getMaxWidthScrollBarAndDownButton() const +{ + tools::Long nButtonDownWidth = 0; + + vcl::Window *pBorder = GetWindow( GetWindowType::Border ); + ImplControlValue aControlValue; + tools::Rectangle aContent, aBound; + + // use the full extent of the control + tools::Rectangle aArea( Point(), pBorder->GetOutputSizePixel() ); + + if ( GetNativeControlRegion(ControlType::Combobox, ControlPart::ButtonDown, + aArea, ControlState::NONE, aControlValue, aBound, aContent) ) + { + nButtonDownWidth = aContent.getWidth(); + } + + tools::Long nScrollBarWidth = GetSettings().GetStyleSettings().GetScrollBarSize(); + + return std::max(nScrollBarWidth, nButtonDownWidth); +} + +Size ComboBox::CalcMinimumSize() const +{ + Size aSz; + + if (!m_pImpl->m_pImplLB) + return aSz; + + if (!IsDropDownBox()) + { + aSz = m_pImpl->m_pImplLB->CalcSize( m_pImpl->m_pImplLB->GetEntryList().GetEntryCount() ); + aSz.AdjustHeight(m_pImpl->m_nDDHeight ); + } + else + { + aSz.setHeight( Edit::CalcMinimumSizeForText(GetText()).Height() ); + + if (m_pImpl->m_nWidthInChars!= -1) + aSz.setWidth(m_pImpl->m_nWidthInChars * approximate_digit_width()); + else + aSz.setWidth(m_pImpl->m_pImplLB->GetMaxEntryWidth()); + } + + if (m_pImpl->m_nMaxWidthChars != -1) + { + tools::Long nMaxWidth = m_pImpl->m_nMaxWidthChars * approximate_char_width(); + aSz.setWidth( std::min(aSz.Width(), nMaxWidth) ); + } + + if (IsDropDownBox()) + aSz.AdjustWidth(getMaxWidthScrollBarAndDownButton() ); + + ComboBoxBounds aBounds(m_pImpl->calcComboBoxDropDownComponentBounds( + Size(0xFFFF, 0xFFFF), Size(0xFFFF, 0xFFFF))); + aSz.AdjustWidth(aBounds.aSubEditPos.X()*2 ); + + aSz.AdjustWidth(ImplGetExtraXOffset() * 2 ); + + aSz = CalcWindowSize( aSz ); + return aSz; +} + +Size ComboBox::CalcAdjustedSize( const Size& rPrefSize ) const +{ + Size aSz = rPrefSize; + sal_Int32 nLeft, nTop, nRight, nBottom; + static_cast<vcl::Window*>(const_cast<ComboBox *>(this))->GetBorder( nLeft, nTop, nRight, nBottom ); + aSz.AdjustHeight( -(nTop+nBottom) ); + if ( !IsDropDownBox() ) + { + tools::Long nEntryHeight = CalcBlockSize( 1, 1 ).Height(); + tools::Long nLines = aSz.Height() / nEntryHeight; + if ( nLines < 1 ) + nLines = 1; + aSz.setHeight( nLines * nEntryHeight ); + aSz.AdjustHeight(m_pImpl->m_nDDHeight ); + } + else + { + aSz.setHeight( m_pImpl->m_nDDHeight ); + } + aSz.AdjustHeight(nTop+nBottom ); + + aSz = CalcWindowSize( aSz ); + return aSz; +} + +Size ComboBox::CalcBlockSize( sal_uInt16 nColumns, sal_uInt16 nLines ) const +{ + // show ScrollBars where appropriate + Size aMinSz = CalcMinimumSize(); + Size aSz; + + // height + if ( nLines ) + { + if ( !IsDropDownBox() ) + aSz.setHeight( m_pImpl->m_pImplLB->CalcSize( nLines ).Height() + m_pImpl->m_nDDHeight ); + else + aSz.setHeight( m_pImpl->m_nDDHeight ); + } + else + aSz.setHeight( aMinSz.Height() ); + + // width + if ( nColumns ) + aSz.setWidth( nColumns * approximate_char_width() ); + else + aSz.setWidth( aMinSz.Width() ); + + if ( IsDropDownBox() ) + aSz.AdjustWidth(getMaxWidthScrollBarAndDownButton() ); + + if ( !IsDropDownBox() ) + { + if ( aSz.Width() < aMinSz.Width() ) + aSz.AdjustHeight(GetSettings().GetStyleSettings().GetScrollBarSize() ); + if ( aSz.Height() < aMinSz.Height() ) + aSz.AdjustWidth(GetSettings().GetStyleSettings().GetScrollBarSize() ); + } + + aSz.AdjustWidth(ImplGetExtraXOffset() * 2 ); + + aSz = CalcWindowSize( aSz ); + return aSz; +} + +tools::Long ComboBox::GetDropDownEntryHeight() const +{ + return m_pImpl->m_pImplLB->GetEntryHeight(); +} + +void ComboBox::GetMaxVisColumnsAndLines( sal_uInt16& rnCols, sal_uInt16& rnLines ) const +{ + tools::Long nCharWidth = GetTextWidth(OUString(u'x')); + if ( !IsDropDownBox() ) + { + Size aOutSz = GetMainWindow()->GetOutputSizePixel(); + rnCols = (nCharWidth > 0) ? static_cast<sal_uInt16>(aOutSz.Width()/nCharWidth) : 1; + rnLines = static_cast<sal_uInt16>(aOutSz.Height()/GetDropDownEntryHeight()); + } + else + { + Size aOutSz = m_pImpl->m_pSubEdit->GetOutputSizePixel(); + rnCols = (nCharWidth > 0) ? static_cast<sal_uInt16>(aOutSz.Width()/nCharWidth) : 1; + rnLines = 1; + } +} + +void ComboBox::Draw( OutputDevice* pDev, const Point& rPos, SystemTextColorFlags nFlags ) +{ + GetMainWindow()->ApplySettings(*pDev); + + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + vcl::Font aFont = GetMainWindow()->GetDrawPixelFont( pDev ); + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + pDev->SetTextFillColor(); + + // Border/Background + pDev->SetLineColor(); + pDev->SetFillColor(); + bool bBorder = (GetStyle() & WB_BORDER); + bool bBackground = IsControlBackground(); + if ( bBorder || bBackground ) + { + tools::Rectangle aRect( aPos, aSize ); + // aRect.Top() += nEditHeight; + if ( bBorder ) + { + ImplDrawFrame( pDev, aRect ); + } + if ( bBackground ) + { + pDev->SetFillColor( GetControlBackground() ); + pDev->DrawRect( aRect ); + } + } + + // contents + if ( !IsDropDownBox() ) + { + tools::Long nOnePixel = GetDrawPixel( pDev, 1 ); + tools::Long nTextHeight = pDev->GetTextHeight(); + tools::Long nEditHeight = nTextHeight + 6*nOnePixel; + DrawTextFlags nTextStyle = DrawTextFlags::VCenter; + + // First, draw the edit part + Size aOrigSize(m_pImpl->m_pSubEdit->GetSizePixel()); + m_pImpl->m_pSubEdit->SetSizePixel(Size(aSize.Width(), nEditHeight)); + m_pImpl->m_pSubEdit->Draw( pDev, aPos, nFlags ); + m_pImpl->m_pSubEdit->SetSizePixel(aOrigSize); + + // Second, draw the listbox + if ( GetStyle() & WB_CENTER ) + nTextStyle |= DrawTextFlags::Center; + else if ( GetStyle() & WB_RIGHT ) + nTextStyle |= DrawTextFlags::Right; + else + nTextStyle |= DrawTextFlags::Left; + + if ( nFlags & SystemTextColorFlags::Mono ) + { + pDev->SetTextColor( COL_BLACK ); + } + else + { + if ( !IsEnabled() ) + { + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + pDev->SetTextColor( rStyleSettings.GetDisableColor() ); + } + else + { + pDev->SetTextColor( GetTextColor() ); + } + } + + tools::Rectangle aClip( aPos, aSize ); + pDev->IntersectClipRegion( aClip ); + sal_Int32 nLines = static_cast<sal_Int32>( nTextHeight > 0 ? (aSize.Height()-nEditHeight)/nTextHeight : 1 ); + if ( !nLines ) + nLines = 1; + const sal_Int32 nTEntry = IsReallyVisible() ? m_pImpl->m_pImplLB->GetTopEntry() : 0; + + tools::Rectangle aTextRect( aPos, aSize ); + + aTextRect.AdjustLeft(3*nOnePixel ); + aTextRect.AdjustRight( -(3*nOnePixel) ); + aTextRect.AdjustTop(nEditHeight + nOnePixel ); + aTextRect.SetBottom( aTextRect.Top() + nTextHeight ); + + // the drawing starts here + for ( sal_Int32 n = 0; n < nLines; ++n ) + { + pDev->DrawText( aTextRect, m_pImpl->m_pImplLB->GetEntryList().GetEntryText( n+nTEntry ), nTextStyle ); + aTextRect.AdjustTop(nTextHeight ); + aTextRect.AdjustBottom(nTextHeight ); + } + } + + pDev->Pop(); + + // Call Edit::Draw after restoring the MapMode... + if ( IsDropDownBox() ) + { + Size aOrigSize(m_pImpl->m_pSubEdit->GetSizePixel()); + m_pImpl->m_pSubEdit->SetSizePixel(GetSizePixel()); + m_pImpl->m_pSubEdit->Draw( pDev, rPos, nFlags ); + m_pImpl->m_pSubEdit->SetSizePixel(aOrigSize); + // DD-Button ? + } +} + +void ComboBox::SetUserDrawHdl(const Link<UserDrawEvent*, void>& rLink) +{ + m_pImpl->m_pImplLB->SetUserDrawHdl(rLink); +} + +void ComboBox::SetUserItemSize( const Size& rSz ) +{ + GetMainWindow()->SetUserItemSize( rSz ); +} + +void ComboBox::EnableUserDraw( bool bUserDraw ) +{ + GetMainWindow()->EnableUserDraw( bUserDraw ); +} + +bool ComboBox::IsUserDrawEnabled() const +{ + return GetMainWindow()->IsUserDrawEnabled(); +} + +void ComboBox::DrawEntry(const UserDrawEvent& rEvt) +{ + GetMainWindow()->DrawEntry(*rEvt.GetRenderContext(), rEvt.GetItemId(), /*bDrawImage*/false, /*bDrawText*/false); +} + +void ComboBox::AddSeparator( sal_Int32 n ) +{ + m_pImpl->m_pImplLB->AddSeparator( n ); +} + +void ComboBox::SetMRUEntries( std::u16string_view rEntries ) +{ + m_pImpl->m_pImplLB->SetMRUEntries( rEntries, ';' ); +} + +OUString ComboBox::GetMRUEntries() const +{ + return m_pImpl->m_pImplLB ? m_pImpl->m_pImplLB->GetMRUEntries( ';' ) : OUString(); +} + +void ComboBox::SetMaxMRUCount( sal_Int32 n ) +{ + m_pImpl->m_pImplLB->SetMaxMRUCount( n ); +} + +sal_Int32 ComboBox::GetMaxMRUCount() const +{ + return m_pImpl->m_pImplLB ? m_pImpl->m_pImplLB->GetMaxMRUCount() : 0; +} + +sal_uInt16 ComboBox::GetDisplayLineCount() const +{ + return m_pImpl->m_pImplLB ? m_pImpl->m_pImplLB->GetDisplayLineCount() : 0; +} + +void ComboBox::SetEntryData( sal_Int32 nPos, void* pNewData ) +{ + m_pImpl->m_pImplLB->SetEntryData( nPos + m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(), pNewData ); +} + +void* ComboBox::GetEntryData( sal_Int32 nPos ) const +{ + return m_pImpl->m_pImplLB->GetEntryList().GetEntryData( + nPos + m_pImpl->m_pImplLB->GetEntryList().GetMRUCount() ); +} + +sal_Int32 ComboBox::GetTopEntry() const +{ + sal_Int32 nPos = GetEntryCount() ? m_pImpl->m_pImplLB->GetTopEntry() : LISTBOX_ENTRY_NOTFOUND; + if (nPos < m_pImpl->m_pImplLB->GetEntryList().GetMRUCount()) + nPos = 0; + return nPos; +} + +tools::Rectangle ComboBox::GetDropDownPosSizePixel() const +{ + return m_pImpl->m_pFloatWin + ? m_pImpl->m_pFloatWin->GetWindowExtentsRelative(this) + : tools::Rectangle(); +} + +const Wallpaper& ComboBox::GetDisplayBackground() const +{ + if (!m_pImpl->m_pSubEdit->IsBackground()) + return Control::GetDisplayBackground(); + + const Wallpaper& rBack = m_pImpl->m_pSubEdit->GetBackground(); + if( ! rBack.IsBitmap() && + ! rBack.IsGradient() && + rBack == Wallpaper(COL_TRANSPARENT) + ) + return Control::GetDisplayBackground(); + return rBack; +} + +sal_Int32 ComboBox::GetSelectedEntryCount() const +{ + return m_pImpl->m_pImplLB->GetEntryList().GetSelectedEntryCount(); +} + +sal_Int32 ComboBox::GetSelectedEntryPos( sal_Int32 nIndex ) const +{ + sal_Int32 nPos = m_pImpl->m_pImplLB->GetEntryList().GetSelectedEntryPos( nIndex ); + if ( nPos != LISTBOX_ENTRY_NOTFOUND ) + { + if (nPos < m_pImpl->m_pImplLB->GetEntryList().GetMRUCount()) + nPos = m_pImpl->m_pImplLB->GetEntryList().FindEntry(m_pImpl->m_pImplLB->GetEntryList().GetEntryText(nPos)); + nPos = sal::static_int_cast<sal_Int32>(nPos - m_pImpl->m_pImplLB->GetEntryList().GetMRUCount()); + } + return nPos; +} + +bool ComboBox::IsEntryPosSelected( sal_Int32 nPos ) const +{ + return m_pImpl->m_pImplLB->GetEntryList().IsEntryPosSelected( + nPos + m_pImpl->m_pImplLB->GetEntryList().GetMRUCount() ); +} + +void ComboBox::SelectEntryPos( sal_Int32 nPos, bool bSelect) +{ + if (nPos < m_pImpl->m_pImplLB->GetEntryList().GetEntryCount()) + m_pImpl->m_pImplLB->SelectEntry( + nPos + m_pImpl->m_pImplLB->GetEntryList().GetMRUCount(), bSelect); +} + +void ComboBox::SetNoSelection() +{ + m_pImpl->m_pImplLB->SetNoSelection(); + m_pImpl->m_pSubEdit->SetText( OUString() ); +} + +tools::Rectangle ComboBox::GetBoundingRectangle( sal_Int32 nItem ) const +{ + tools::Rectangle aRect = GetMainWindow()->GetBoundingRectangle( nItem ); + tools::Rectangle aOffset = GetMainWindow()->GetWindowExtentsRelative( static_cast<vcl::Window*>(const_cast<ComboBox *>(this)) ); + aRect.Move( aOffset.Left(), aOffset.Top() ); + return aRect; +} + +void ComboBox::SetBorderStyle( WindowBorderStyle nBorderStyle ) +{ + Window::SetBorderStyle( nBorderStyle ); + if ( !IsDropDownBox() ) + { + m_pImpl->m_pSubEdit->SetBorderStyle( nBorderStyle ); + m_pImpl->m_pImplLB->SetBorderStyle( nBorderStyle ); + } +} + +ImplListBoxWindow* ComboBox::GetMainWindow() const +{ + return m_pImpl->m_pImplLB->GetMainWindow(); +} + +tools::Long ComboBox::GetIndexForPoint( const Point& rPoint, sal_Int32& rPos ) const +{ + if( !HasLayoutData() ) + FillLayoutData(); + + // check whether rPoint fits at all + tools::Long nIndex = Control::GetIndexForPoint( rPoint ); + if( nIndex != -1 ) + { + // point must be either in main list window + // or in impl window (dropdown case) + ImplListBoxWindow* rMain = GetMainWindow(); + + // convert coordinates to ImplListBoxWindow pixel coordinate space + Point aConvPoint = LogicToPixel( rPoint ); + aConvPoint = OutputToAbsoluteScreenPixel( aConvPoint ); + aConvPoint = rMain->AbsoluteScreenToOutputPixel( aConvPoint ); + aConvPoint = rMain->PixelToLogic( aConvPoint ); + + // try to find entry + sal_Int32 nEntry = rMain->GetEntryPosForPoint( aConvPoint ); + if( nEntry == LISTBOX_ENTRY_NOTFOUND ) + nIndex = -1; + else + rPos = nEntry; + } + + // get line relative index + if( nIndex != -1 ) + nIndex = ToRelativeLineIndex( nIndex ); + + return nIndex; +} + +ComboBoxBounds ComboBox::Impl::calcComboBoxDropDownComponentBounds( + const Size &rOutSz, const Size &rBorderOutSz) const +{ + ComboBoxBounds aBounds; + + tools::Long nTop = 0; + tools::Long nBottom = rOutSz.Height(); + + vcl::Window *pBorder = m_rThis.GetWindow( GetWindowType::Border ); + ImplControlValue aControlValue; + Point aPoint; + tools::Rectangle aContent, aBound; + + // use the full extent of the control + tools::Rectangle aArea( aPoint, rBorderOutSz ); + + if (m_rThis.GetNativeControlRegion(ControlType::Combobox, ControlPart::ButtonDown, + aArea, ControlState::NONE, aControlValue, aBound, aContent) ) + { + // convert back from border space to local coordinates + aPoint = pBorder->ScreenToOutputPixel(m_rThis.OutputToScreenPixel(aPoint)); + aContent.Move(-aPoint.X(), -aPoint.Y()); + + aBounds.aButtonPos = Point(aContent.Left(), nTop); + aBounds.aButtonSize = Size(aContent.getWidth(), (nBottom-nTop)); + + // adjust the size of the edit field + if (m_rThis.GetNativeControlRegion(ControlType::Combobox, ControlPart::SubEdit, + aArea, ControlState::NONE, aControlValue, aBound, aContent) ) + { + // convert back from border space to local coordinates + aContent.Move(-aPoint.X(), -aPoint.Y()); + + // use the themes drop down size + aBounds.aSubEditPos = aContent.TopLeft(); + aBounds.aSubEditSize = aContent.GetSize(); + } + else + { + // use the themes drop down size for the button + aBounds.aSubEditSize = Size(rOutSz.Width() - aContent.getWidth(), rOutSz.Height()); + } + } + else + { + tools::Long nSBWidth = m_rThis.GetSettings().GetStyleSettings().GetScrollBarSize(); + nSBWidth = m_rThis.CalcZoom( nSBWidth ); + aBounds.aSubEditSize = Size(rOutSz.Width() - nSBWidth, rOutSz.Height()); + aBounds.aButtonPos = Point(rOutSz.Width() - nSBWidth, nTop); + aBounds.aButtonSize = Size(nSBWidth, (nBottom-nTop)); + } + return aBounds; +} + +void ComboBox::SetWidthInChars(sal_Int32 nWidthInChars) +{ + if (nWidthInChars != m_pImpl->m_nWidthInChars) + { + m_pImpl->m_nWidthInChars = nWidthInChars; + queue_resize(); + } +} + +void ComboBox::setMaxWidthChars(sal_Int32 nWidth) +{ + if (nWidth != m_pImpl->m_nMaxWidthChars) + { + m_pImpl->m_nMaxWidthChars = nWidth; + queue_resize(); + } +} + +bool ComboBox::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "width-chars") + SetWidthInChars(rValue.toInt32()); + else if (rKey == "max-width-chars") + setMaxWidthChars(rValue.toInt32()); + else if (rKey == "can-focus") + { + // as far as I can see in Gtk, setting a ComboBox as can.focus means + // the focus gets stuck in it, so try here to behave like gtk does + // with the settings that work, i.e. can.focus of false doesn't + // set the hard WB_NOTABSTOP + WinBits nBits = GetStyle(); + nBits &= ~(WB_TABSTOP|WB_NOTABSTOP); + if (toBool(rValue)) + nBits |= WB_TABSTOP; + SetStyle(nBits); + } + else if (rKey == "placeholder-text") + SetPlaceholderText(rValue); + else + return Control::set_property(rKey, rValue); + return true; +} + +FactoryFunction ComboBox::GetUITestFactory() const +{ + return ComboBoxUIObject::create; +} + +void ComboBox::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Control::DumpAsPropertyTree(rJsonWriter); + + { + auto entriesNode = rJsonWriter.startArray("entries"); + for (int i = 0; i < GetEntryCount(); ++i) + { + rJsonWriter.putSimpleValue(GetEntry(i)); + } + } + + { + auto selectedNode = rJsonWriter.startArray("selectedEntries"); + for (int i = 0; i < GetSelectedEntryCount(); ++i) + { + rJsonWriter.putSimpleValue(OUString::number(GetSelectedEntryPos(i))); + } + } + + rJsonWriter.put("selectedCount", GetSelectedEntryCount()); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/ctrl.cxx b/vcl/source/control/ctrl.cxx new file mode 100644 index 000000000..6cb06bc11 --- /dev/null +++ b/vcl/source/control/ctrl.cxx @@ -0,0 +1,505 @@ +/* -*- 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 <o3tl/safeint.hxx> +#include <vcl/svapp.hxx> +#include <vcl/event.hxx> +#include <vcl/ctrl.hxx> +#include <vcl/decoview.hxx> +#include <vcl/settings.hxx> +#include <vcl/uitest/logger.hxx> +#include <sal/log.hxx> + +#include <textlayout.hxx> +#include <svdata.hxx> + +using namespace vcl; + +void Control::ImplInitControlData() +{ + mbHasControlFocus = false; + mbShowAccelerator = false; +} + +Control::Control( WindowType nType ) : + Window( nType ) +{ + ImplInitControlData(); +} + +Control::Control( vcl::Window* pParent, WinBits nStyle ) : + Window( WindowType::CONTROL ) +{ + ImplInitControlData(); + ImplInit( pParent, nStyle, nullptr ); +} + +Control::~Control() +{ + disposeOnce(); +} + +void Control::dispose() +{ + mxLayoutData.reset(); + mpReferenceDevice.clear(); + Window::dispose(); +} + +void Control::EnableRTL( bool bEnable ) +{ + // convenience: for controls also switch layout mode + GetOutDev()->SetLayoutMode( bEnable ? vcl::text::ComplexTextLayoutFlags::BiDiRtl | vcl::text::ComplexTextLayoutFlags::TextOriginLeft : + vcl::text::ComplexTextLayoutFlags::TextOriginLeft ); + CompatStateChanged( StateChangedType::Mirroring ); + Window::EnableRTL(bEnable); +} + +void Control::Resize() +{ + ImplClearLayoutData(); + Window::Resize(); +} + +void Control::FillLayoutData() const +{ +} + +void Control::CreateLayoutData() const +{ + SAL_WARN_IF( mxLayoutData, "vcl", "Control::CreateLayoutData: should be called with non-existent layout data only!" ); + mxLayoutData.emplace(); +} + +bool Control::HasLayoutData() const +{ + return bool(mxLayoutData); +} + +void Control::SetText( const OUString& rStr ) +{ + ImplClearLayoutData(); + Window::SetText( rStr ); +} + +ControlLayoutData::ControlLayoutData() : m_pParent( nullptr ) +{ +} + +tools::Rectangle ControlLayoutData::GetCharacterBounds( tools::Long nIndex ) const +{ + return (nIndex >= 0 && o3tl::make_unsigned(nIndex) < m_aUnicodeBoundRects.size()) ? m_aUnicodeBoundRects[ nIndex ] : tools::Rectangle(); +} + +tools::Rectangle Control::GetCharacterBounds( tools::Long nIndex ) const +{ + if( !HasLayoutData() ) + FillLayoutData(); + return mxLayoutData ? mxLayoutData->GetCharacterBounds( nIndex ) : tools::Rectangle(); +} + +tools::Long ControlLayoutData::GetIndexForPoint( const Point& rPoint ) const +{ + tools::Long nIndex = -1; + for( tools::Long i = m_aUnicodeBoundRects.size()-1; i >= 0; i-- ) + { + Point aTopLeft = m_aUnicodeBoundRects[i].TopLeft(); + Point aBottomRight = m_aUnicodeBoundRects[i].BottomRight(); + if (rPoint.X() >= aTopLeft.X() && rPoint.Y() >= aTopLeft.Y() && + rPoint.X() <= aBottomRight.X() && rPoint.Y() <= aBottomRight.Y()) + { + nIndex = i; + break; + } + } + return nIndex; +} + +tools::Long Control::GetIndexForPoint( const Point& rPoint ) const +{ + if( ! HasLayoutData() ) + FillLayoutData(); + return mxLayoutData ? mxLayoutData->GetIndexForPoint( rPoint ) : -1; +} + +tools::Long ControlLayoutData::GetLineCount() const +{ + tools::Long nLines = m_aLineIndices.size(); + if( nLines == 0 && !m_aDisplayText.isEmpty() ) + nLines = 1; + return nLines; +} + +Pair ControlLayoutData::GetLineStartEnd( tools::Long nLine ) const +{ + Pair aPair( -1, -1 ); + + int nDisplayLines = m_aLineIndices.size(); + if( nLine >= 0 && nLine < nDisplayLines ) + { + aPair.A() = m_aLineIndices[nLine]; + if( nLine+1 < nDisplayLines ) + aPair.B() = m_aLineIndices[nLine+1]-1; + else + aPair.B() = m_aDisplayText.getLength()-1; + } + else if( nLine == 0 && nDisplayLines == 0 && !m_aDisplayText.isEmpty() ) + { + // special case for single line controls so the implementations + // in that case do not have to fill in the line indices + aPair.A() = 0; + aPair.B() = m_aDisplayText.getLength()-1; + } + return aPair; +} + +Pair Control::GetLineStartEnd( tools::Long nLine ) const +{ + if( !HasLayoutData() ) + FillLayoutData(); + return mxLayoutData ? mxLayoutData->GetLineStartEnd( nLine ) : Pair( -1, -1 ); +} + +tools::Long ControlLayoutData::ToRelativeLineIndex( tools::Long nIndex ) const +{ + // is the index sensible at all ? + if( nIndex >= 0 && nIndex < m_aDisplayText.getLength() ) + { + int nDisplayLines = m_aLineIndices.size(); + // if only 1 line exists, then absolute and relative index are + // identical -> do nothing + if( nDisplayLines > 1 ) + { + int nLine; + for( nLine = nDisplayLines-1; nLine >= 0; nLine-- ) + { + if( m_aLineIndices[nLine] <= nIndex ) + { + nIndex -= m_aLineIndices[nLine]; + break; + } + } + if( nLine < 0 ) + { + SAL_WARN_IF( nLine < 0, "vcl", "ToRelativeLineIndex failed" ); + nIndex = -1; + } + } + } + else + nIndex = -1; + + return nIndex; +} + +tools::Long Control::ToRelativeLineIndex( tools::Long nIndex ) const +{ + if( !HasLayoutData() ) + FillLayoutData(); + return mxLayoutData ? mxLayoutData->ToRelativeLineIndex( nIndex ) : -1; +} + +OUString Control::GetDisplayText() const +{ + if( !HasLayoutData() ) + FillLayoutData(); + return mxLayoutData ? mxLayoutData->m_aDisplayText : GetText(); +} + +bool Control::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + { + if ( !mbHasControlFocus ) + { + mbHasControlFocus = true; + CompatStateChanged( StateChangedType::ControlFocus ); + if ( ImplCallEventListenersAndHandler( VclEventId::ControlGetFocus, {} ) ) + // been destroyed within the handler + return true; + } + } + else + { + if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + vcl::Window* pFocusWin = Application::GetFocusWindow(); + if ( !pFocusWin || !ImplIsWindowOrChild( pFocusWin ) ) + { + mbHasControlFocus = false; + CompatStateChanged( StateChangedType::ControlFocus ); + if ( ImplCallEventListenersAndHandler( VclEventId::ControlLoseFocus, [this] () { maLoseFocusHdl.Call(*this); } ) ) + // been destroyed within the handler + return true; + } + } + } + return Window::EventNotify( rNEvt ); +} + +void Control::StateChanged( StateChangedType nStateChange ) +{ + if( nStateChange == StateChangedType::InitShow || + nStateChange == StateChangedType::Visible || + nStateChange == StateChangedType::Zoom || + nStateChange == StateChangedType::ControlFont + ) + { + ImplClearLayoutData(); + } + Window::StateChanged( nStateChange ); +} + +void Control::AppendLayoutData( const Control& rSubControl ) const +{ + if( !rSubControl.HasLayoutData() ) + rSubControl.FillLayoutData(); + if( !rSubControl.HasLayoutData() || rSubControl.mxLayoutData->m_aDisplayText.isEmpty() ) + return; + + tools::Long nCurrentIndex = mxLayoutData->m_aDisplayText.getLength(); + mxLayoutData->m_aDisplayText += rSubControl.mxLayoutData->m_aDisplayText; + int nLines = rSubControl.mxLayoutData->m_aLineIndices.size(); + int n; + mxLayoutData->m_aLineIndices.push_back( nCurrentIndex ); + for( n = 1; n < nLines; n++ ) + mxLayoutData->m_aLineIndices.push_back( rSubControl.mxLayoutData->m_aLineIndices[n] + nCurrentIndex ); + int nRectangles = rSubControl.mxLayoutData->m_aUnicodeBoundRects.size(); + tools::Rectangle aRel = rSubControl.GetWindowExtentsRelative(this); + for( n = 0; n < nRectangles; n++ ) + { + tools::Rectangle aRect = rSubControl.mxLayoutData->m_aUnicodeBoundRects[n]; + aRect.Move( aRel.Left(), aRel.Top() ); + mxLayoutData->m_aUnicodeBoundRects.push_back( aRect ); + } +} + +void Control::CallEventListeners( VclEventId nEvent, void* pData) +{ + VclPtr<Control> xThis(this); + UITestLogger::getInstance().logAction(xThis, nEvent); + + vcl::Window::CallEventListeners(nEvent, pData); +} + +bool Control::ImplCallEventListenersAndHandler( VclEventId nEvent, std::function<void()> const & callHandler ) +{ + VclPtr<Control> xThis(this); + + Control::CallEventListeners( nEvent ); + + if ( !xThis->isDisposed() ) + { + if (callHandler) + { + callHandler(); + } + + if ( !xThis->isDisposed() ) + return false; + } + return true; +} + +void Control::SetLayoutDataParent( const Control* pParent ) const +{ + if( HasLayoutData() ) + mxLayoutData->m_pParent = pParent; +} + +void Control::ImplClearLayoutData() const +{ + mxLayoutData.reset(); +} + +void Control::ImplDrawFrame( OutputDevice* pDev, tools::Rectangle& rRect ) +{ + // use a deco view to draw the frame + // However, since there happens a lot of magic there, we need to fake some (style) settings + // on the device + AllSettings aOriginalSettings( pDev->GetSettings() ); + + AllSettings aNewSettings( aOriginalSettings ); + StyleSettings aStyle( aNewSettings.GetStyleSettings() ); + + // The *only known* clients of the Draw methods of the various VCL-controls are form controls: + // During print preview, and during printing, Draw is called. Thus, drawing always happens with a + // mono (colored) border + aStyle.SetOptions( aStyle.GetOptions() | StyleSettingsOptions::Mono ); + aStyle.SetMonoColor( GetSettings().GetStyleSettings().GetMonoColor() ); + + aNewSettings.SetStyleSettings( aStyle ); + // #i67023# do not call data changed listeners for this fake + // since they may understandably invalidate on settings changed + pDev->OutputDevice::SetSettings( aNewSettings ); + + DecorationView aDecoView( pDev ); + rRect = aDecoView.DrawFrame( rRect, DrawFrameStyle::Out, DrawFrameFlags::WindowBorder ); + + pDev->OutputDevice::SetSettings( aOriginalSettings ); +} + +void Control::SetShowAccelerator(bool bVal) +{ + mbShowAccelerator = bVal; +}; + +ControlLayoutData::~ControlLayoutData() +{ + if( m_pParent ) + m_pParent->ImplClearLayoutData(); +} + +Size Control::GetOptimalSize() const +{ + return Size( GetTextWidth( GetText() ) + 2 * 12, + GetTextHeight() + 2 * 6 ); +} + +void Control::SetReferenceDevice( OutputDevice* _referenceDevice ) +{ + if ( mpReferenceDevice == _referenceDevice ) + return; + + mpReferenceDevice = _referenceDevice; + Invalidate(); +} + +OutputDevice* Control::GetReferenceDevice() const +{ + // tdf#118377 It can happen that mpReferenceDevice is already disposed and + // stays disposed (see task, even when Dialog is closed). I have no idea if + // this may be very bad - someone who knows more about lifetime of OutputDevice's + // will have to decide. + // To secure this, I changed all accesses to mpControlData->mpReferenceDevice to + // use Control::GetReferenceDevice() - only use mpControlData->mpReferenceDevice + // inside Control::SetReferenceDevice and Control::GetReferenceDevice(). + // Control::GetReferenceDevice() will now reset mpReferenceDevice if it is already + // disposed. This way all usages will do a kind of 'test-and-get' call. + if(nullptr != mpReferenceDevice && mpReferenceDevice->isDisposed()) + { + const_cast<Control*>(this)->SetReferenceDevice(nullptr); + } + + return mpReferenceDevice; +} + +const vcl::Font& Control::GetCanonicalFont( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetLabelFont(); +} + +const Color& Control::GetCanonicalTextColor( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetLabelTextColor(); +} + +void Control::ApplySettings(vcl::RenderContext& rRenderContext) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + ApplyControlFont(rRenderContext, GetCanonicalFont(rStyleSettings)); + + ApplyControlForeground(rRenderContext, GetCanonicalTextColor(rStyleSettings)); + rRenderContext.SetTextFillColor(); +} + +void Control::ImplInitSettings() +{ + ApplySettings(*GetOutDev()); +} + +tools::Rectangle Control::DrawControlText( OutputDevice& _rTargetDevice, const tools::Rectangle& rRect, const OUString& _rStr, + DrawTextFlags _nStyle, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText, const Size* i_pDeviceSize ) const +{ + OUString rPStr = _rStr; + DrawTextFlags nPStyle = _nStyle; + + bool autoacc = ImplGetSVData()->maNWFData.mbAutoAccel; + + if (autoacc && !mbShowAccelerator) + { + rPStr = GetNonMnemonicString( _rStr ); + nPStyle &= ~DrawTextFlags::HideMnemonic; + } + + if( !GetReferenceDevice() || ( GetReferenceDevice() == &_rTargetDevice ) ) + { + const tools::Rectangle aRet = _rTargetDevice.GetTextRect(rRect, rPStr, nPStyle); + _rTargetDevice.DrawText(aRet, rPStr, nPStyle, _pVector, _pDisplayText); + return aRet; + } + + ControlTextRenderer aRenderer( *this, _rTargetDevice, *GetReferenceDevice() ); + return aRenderer.DrawText(rRect, rPStr, nPStyle, _pVector, _pDisplayText, i_pDeviceSize); +} + +tools::Rectangle Control::GetControlTextRect( OutputDevice& _rTargetDevice, const tools::Rectangle & rRect, + const OUString& _rStr, DrawTextFlags _nStyle, Size* o_pDeviceSize ) const +{ + OUString rPStr = _rStr; + DrawTextFlags nPStyle = _nStyle; + + bool autoacc = ImplGetSVData()->maNWFData.mbAutoAccel; + + if (autoacc && !mbShowAccelerator) + { + rPStr = GetNonMnemonicString( _rStr ); + nPStyle &= ~DrawTextFlags::HideMnemonic; + } + + if ( !GetReferenceDevice() || ( GetReferenceDevice() == &_rTargetDevice ) ) + { + tools::Rectangle aRet = _rTargetDevice.GetTextRect( rRect, rPStr, nPStyle ); + if (o_pDeviceSize) + { + *o_pDeviceSize = aRet.GetSize(); + } + return aRet; + } + + ControlTextRenderer aRenderer( *this, _rTargetDevice, *GetReferenceDevice() ); + return aRenderer.GetTextRect(rRect, rPStr, nPStyle, o_pDeviceSize); +} + +Font +Control::GetUnzoomedControlPointFont() const +{ + Font aFont(GetCanonicalFont(GetSettings().GetStyleSettings())); + if (IsControlFont()) + aFont.Merge(GetControlFont()); + return aFont; +} + +void Control::LogicMouseButtonDown(const MouseEvent& rEvent) +{ + MouseButtonDown(rEvent); +} + +void Control::LogicMouseButtonUp(const MouseEvent& rEvent) +{ + MouseButtonUp(rEvent); +} + +void Control::LogicMouseMove(const MouseEvent& rEvent) +{ + MouseMove(rEvent); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/edit.cxx b/vcl/source/control/edit.cxx new file mode 100644 index 000000000..72b55c528 --- /dev/null +++ b/vcl/source/control/edit.cxx @@ -0,0 +1,2929 @@ +/* -*- 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 <vcl/builder.hxx> +#include <vcl/event.hxx> +#include <vcl/cursor.hxx> +#include <vcl/menu.hxx> +#include <vcl/toolkit/edit.hxx> +#include <vcl/weld.hxx> +#include <vcl/specialchars.hxx> +#include <vcl/svapp.hxx> +#include <vcl/settings.hxx> +#include <vcl/transfer.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <vcl/ptrstyle.hxx> + +#include <window.h> +#include <svdata.hxx> +#include <strings.hrc> + +#include <com/sun/star/i18n/BreakIterator.hpp> +#include <com/sun/star/i18n/CharacterIteratorMode.hpp> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/datatransfer/XTransferable.hpp> +#include <com/sun/star/datatransfer/clipboard/XClipboard.hpp> + +#include <com/sun/star/datatransfer/dnd/DNDConstants.hpp> +#include <com/sun/star/datatransfer/dnd/XDragGestureRecognizer.hpp> +#include <com/sun/star/datatransfer/dnd/XDropTarget.hpp> + +#include <com/sun/star/i18n/InputSequenceChecker.hpp> +#include <com/sun/star/i18n/InputSequenceCheckMode.hpp> +#include <com/sun/star/i18n/ScriptType.hpp> + +#include <com/sun/star/uno/Any.hxx> + +#include <comphelper/processfactory.hxx> +#include <comphelper/string.hxx> + +#include <sot/exchange.hxx> +#include <sot/formats.hxx> +#include <sal/macros.h> +#include <sal/log.hxx> + +#include <i18nlangtag/languagetag.hxx> +#include <vcl/unohelp2.hxx> +#include <o3tl/safeint.hxx> +#include <o3tl/string_view.hxx> +#include <officecfg/Office/Common.hxx> +#include <tools/json_writer.hxx> + +#include <algorithm> +#include <memory> +#include <string_view> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::lang; + +// - Redo +// - if Tracking-Cancel recreate DefaultSelection + +static FncGetSpecialChars pImplFncGetSpecialChars = nullptr; + +#define EDIT_ALIGN_LEFT 1 +#define EDIT_ALIGN_CENTER 2 +#define EDIT_ALIGN_RIGHT 3 + +#define EDIT_DEL_LEFT 1 +#define EDIT_DEL_RIGHT 2 + +#define EDIT_DELMODE_SIMPLE 11 +#define EDIT_DELMODE_RESTOFWORD 12 +#define EDIT_DELMODE_RESTOFCONTENT 13 + +struct DDInfo +{ + vcl::Cursor aCursor; + Selection aDndStartSel; + sal_Int32 nDropPos; + bool bStarterOfDD; + bool bDroppedInMe; + bool bVisCursor; + bool bIsStringSupported; + + DDInfo() + { + aCursor.SetStyle( CURSOR_SHADOW ); + nDropPos = 0; + bStarterOfDD = false; + bDroppedInMe = false; + bVisCursor = false; + bIsStringSupported = false; + } +}; + +struct Impl_IMEInfos +{ + OUString aOldTextAfterStartPos; + std::unique_ptr<ExtTextInputAttr[]> + pAttribs; + sal_Int32 nPos; + sal_Int32 nLen; + bool bCursor; + bool bWasCursorOverwrite; + + Impl_IMEInfos(sal_Int32 nPos, const OUString& rOldTextAfterStartPos); + + void CopyAttribs(const ExtTextInputAttr* pA, sal_Int32 nL); + void DestroyAttribs(); +}; + +Impl_IMEInfos::Impl_IMEInfos(sal_Int32 nP, const OUString& rOldTextAfterStartPos) + : aOldTextAfterStartPos(rOldTextAfterStartPos), + nPos(nP), + nLen(0), + bCursor(true), + bWasCursorOverwrite(false) +{ +} + +void Impl_IMEInfos::CopyAttribs(const ExtTextInputAttr* pA, sal_Int32 nL) +{ + nLen = nL; + pAttribs.reset(new ExtTextInputAttr[ nL ]); + memcpy( pAttribs.get(), pA, nL*sizeof(ExtTextInputAttr) ); +} + +void Impl_IMEInfos::DestroyAttribs() +{ + pAttribs.reset(); + nLen = 0; +} + +Edit::Edit( WindowType nType ) + : Control( nType ) +{ + ImplInitEditData(); +} + +Edit::Edit( vcl::Window* pParent, WinBits nStyle ) + : Control( WindowType::EDIT ) +{ + ImplInitEditData(); + ImplInit( pParent, nStyle ); +} + +void Edit::SetWidthInChars(sal_Int32 nWidthInChars) +{ + if (mnWidthInChars != nWidthInChars) + { + mnWidthInChars = nWidthInChars; + queue_resize(); + } +} + +void Edit::setMaxWidthChars(sal_Int32 nWidth) +{ + if (nWidth != mnMaxWidthChars) + { + mnMaxWidthChars = nWidth; + queue_resize(); + } +} + +bool Edit::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "width-chars") + SetWidthInChars(rValue.toInt32()); + else if (rKey == "max-width-chars") + setMaxWidthChars(rValue.toInt32()); + else if (rKey == "max-length") + { + sal_Int32 nTextLen = rValue.toInt32(); + SetMaxTextLen(nTextLen == 0 ? EDIT_NOLIMIT : nTextLen); + } + else if (rKey == "editable") + { + SetReadOnly(!toBool(rValue)); + } + else if (rKey == "overwrite-mode") + { + SetInsertMode(!toBool(rValue)); + } + else if (rKey == "visibility") + { + mbPassword = false; + if (!toBool(rValue)) + mbPassword = true; + } + else if (rKey == "placeholder-text") + SetPlaceholderText(rValue); + else if (rKey == "shadow-type") + { + if (GetStyle() & WB_BORDER) + SetBorderStyle(rValue == "none" ? WindowBorderStyle::MONO : WindowBorderStyle::NORMAL); + } + else + return Control::set_property(rKey, rValue); + return true; +} + +Edit::~Edit() +{ + disposeOnce(); +} + +void Edit::dispose() +{ + mpUIBuilder.reset(); + mpDDInfo.reset(); + + vcl::Cursor* pCursor = GetCursor(); + if ( pCursor ) + { + SetCursor( nullptr ); + delete pCursor; + } + + mpIMEInfos.reset(); + + if ( mxDnDListener.is() ) + { + if ( GetDragGestureRecognizer().is() ) + { + uno::Reference< datatransfer::dnd::XDragGestureListener> xDGL( mxDnDListener, uno::UNO_QUERY ); + GetDragGestureRecognizer()->removeDragGestureListener( xDGL ); + } + if ( GetDropTarget().is() ) + { + uno::Reference< datatransfer::dnd::XDropTargetListener> xDTL( mxDnDListener, uno::UNO_QUERY ); + GetDropTarget()->removeDropTargetListener( xDTL ); + } + + mxDnDListener->disposing( lang::EventObject() ); // #95154# #96585# Empty Source means it's the Client + mxDnDListener.clear(); + } + + SetType(WindowType::WINDOW); + + mpSubEdit.disposeAndClear(); + Control::dispose(); +} + +void Edit::ImplInitEditData() +{ + mpSubEdit = VclPtr<Edit>(); + mpFilterText = nullptr; + mnXOffset = 0; + mnAlign = EDIT_ALIGN_LEFT; + mnMaxTextLen = EDIT_NOLIMIT; + mnWidthInChars = -1; + mnMaxWidthChars = -1; + mbInternModified = false; + mbReadOnly = false; + mbInsertMode = true; + mbClickedInSelection = false; + mbActivePopup = false; + mbIsSubEdit = false; + mbForceControlBackground = false; + mbPassword = false; + mpDDInfo = nullptr; + mpIMEInfos = nullptr; + mcEchoChar = 0; + + // no default mirroring for Edit controls + // note: controls that use a subedit will revert this (SpinField, ComboBox) + EnableRTL( false ); + + mxDnDListener = new vcl::unohelper::DragAndDropWrapper( this ); +} + +bool Edit::ImplUseNativeBorder(vcl::RenderContext const & rRenderContext, WinBits nStyle) const +{ + bool bRet = rRenderContext.IsNativeControlSupported(ImplGetNativeControlType(), + ControlPart::HasBackgroundTexture) + && ((nStyle & WB_BORDER) && !(nStyle & WB_NOBORDER)); + if (!bRet && mbIsSubEdit) + { + vcl::Window* pWindow = GetParent(); + nStyle = pWindow->GetStyle(); + bRet = pWindow->IsNativeControlSupported(ImplGetNativeControlType(), + ControlPart::HasBackgroundTexture) + && ((nStyle & WB_BORDER) && !(nStyle & WB_NOBORDER)); + } + return bRet; +} + +void Edit::ImplInit(vcl::Window* pParent, WinBits nStyle) +{ + nStyle = ImplInitStyle(nStyle); + + if (!(nStyle & (WB_CENTER | WB_RIGHT))) + nStyle |= WB_LEFT; + + Control::ImplInit(pParent, nStyle, nullptr); + + mbReadOnly = (nStyle & WB_READONLY) != 0; + + mnAlign = EDIT_ALIGN_LEFT; + + // hack: right align until keyinput and cursor travelling works + if( IsRTLEnabled() ) + mnAlign = EDIT_ALIGN_RIGHT; + + if ( nStyle & WB_RIGHT ) + mnAlign = EDIT_ALIGN_RIGHT; + else if ( nStyle & WB_CENTER ) + mnAlign = EDIT_ALIGN_CENTER; + + SetCursor( new vcl::Cursor ); + + SetPointer( PointerStyle::Text ); + ApplySettings(*GetOutDev()); + + uno::Reference< datatransfer::dnd::XDragGestureListener> xDGL( mxDnDListener, uno::UNO_QUERY ); + uno::Reference< datatransfer::dnd::XDragGestureRecognizer > xDGR = GetDragGestureRecognizer(); + if ( xDGR.is() ) + { + xDGR->addDragGestureListener( xDGL ); + uno::Reference< datatransfer::dnd::XDropTargetListener> xDTL( mxDnDListener, uno::UNO_QUERY ); + GetDropTarget()->addDropTargetListener( xDTL ); + GetDropTarget()->setActive( true ); + GetDropTarget()->setDefaultActions( datatransfer::dnd::DNDConstants::ACTION_COPY_OR_MOVE ); + } +} + +WinBits Edit::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + + return nStyle; +} + +bool Edit::IsCharInput( const KeyEvent& rKeyEvent ) +{ + // In the future we must use new Unicode functions for this + sal_Unicode cCharCode = rKeyEvent.GetCharCode(); + return ((cCharCode >= 32) && (cCharCode != 127) && + !rKeyEvent.GetKeyCode().IsMod3() && + !rKeyEvent.GetKeyCode().IsMod2() && + !rKeyEvent.GetKeyCode().IsMod1() ); +} + +void Edit::ApplySettings(vcl::RenderContext& rRenderContext) +{ + Control::ApplySettings(rRenderContext); + + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + const vcl::Font& aFont = rStyleSettings.GetFieldFont(); + ApplyControlFont(rRenderContext, aFont); + + ImplClearLayoutData(); + + Color aTextColor = rStyleSettings.GetFieldTextColor(); + ApplyControlForeground(rRenderContext, aTextColor); + + if (IsControlBackground()) + { + rRenderContext.SetBackground(GetControlBackground()); + rRenderContext.SetFillColor(GetControlBackground()); + + if (ImplUseNativeBorder(rRenderContext, GetStyle())) + { + // indicates that no non-native drawing of background should take place + mpWindowImpl->mnNativeBackground = ControlPart::Entire; + } + } + else if (ImplUseNativeBorder(rRenderContext, GetStyle())) + { + // Transparent background + rRenderContext.SetBackground(); + rRenderContext.SetFillColor(); + } + else + { + rRenderContext.SetBackground(rStyleSettings.GetFieldColor()); + rRenderContext.SetFillColor(rStyleSettings.GetFieldColor()); + } +} + +tools::Long Edit::ImplGetExtraXOffset() const +{ + // MT 09/2002: nExtraOffsetX should become a member, instead of checking every time, + // but I need an incompatible update for this... + // #94095# Use extra offset only when edit has a border + tools::Long nExtraOffset = 0; + if( ( GetStyle() & WB_BORDER ) || ( mbIsSubEdit && ( GetParent()->GetStyle() & WB_BORDER ) ) ) + nExtraOffset = 2; + + return nExtraOffset; +} + +tools::Long Edit::ImplGetExtraYOffset() const +{ + tools::Long nExtraOffset = 0; + ControlType eCtrlType = ImplGetNativeControlType(); + if (eCtrlType != ControlType::EditboxNoBorder) + { + // add some space between text entry and border + nExtraOffset = 2; + } + return nExtraOffset; +} + +OUString Edit::ImplGetText() const +{ + if ( mcEchoChar || mbPassword ) + { + sal_Unicode cEchoChar; + if ( mcEchoChar ) + cEchoChar = mcEchoChar; + else + cEchoChar = u'\x2022'; + OUStringBuffer aText(maText.getLength()); + comphelper::string::padToLength(aText, maText.getLength(), cEchoChar); + return aText.makeStringAndClear(); + } + else + return maText.toString(); +} + +void Edit::ImplInvalidateOrRepaint() +{ + if( IsPaintTransparent() ) + { + Invalidate(); + // FIXME: this is currently only on macOS + if( ImplGetSVData()->maNWFData.mbNoFocusRects ) + PaintImmediately(); + } + else + Invalidate(); +} + +tools::Long Edit::ImplGetTextYPosition() const +{ + if ( GetStyle() & WB_TOP ) + return ImplGetExtraXOffset(); + else if ( GetStyle() & WB_BOTTOM ) + return GetOutputSizePixel().Height() - GetTextHeight() - ImplGetExtraXOffset(); + return ( GetOutputSizePixel().Height() - GetTextHeight() ) / 2; +} + +void Edit::ImplRepaint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRectangle) +{ + if (!IsReallyVisible()) + return; + + ApplySettings(rRenderContext); + + const OUString aText = ImplGetText(); + const sal_Int32 nLen = aText.getLength(); + + sal_Int32 nDXBuffer[256]; + std::unique_ptr<sal_Int32[]> pDXBuffer; + sal_Int32* pDX = nDXBuffer; + + if (nLen) + { + if (o3tl::make_unsigned(2 * nLen) > SAL_N_ELEMENTS(nDXBuffer)) + { + pDXBuffer.reset(new sal_Int32[2 * (nLen + 1)]); + pDX = pDXBuffer.get(); + } + + GetOutDev()->GetCaretPositions(aText, pDX, 0, nLen); + } + + tools::Long nTH = GetTextHeight(); + Point aPos(mnXOffset, ImplGetTextYPosition()); + + vcl::Cursor* pCursor = GetCursor(); + bool bVisCursor = pCursor && pCursor->IsVisible(); + if (pCursor) + pCursor->Hide(); + + ImplClearBackground(rRenderContext, rRectangle, 0, GetOutputSizePixel().Width()-1); + + bool bPaintPlaceholderText = aText.isEmpty() && !maPlaceholderText.isEmpty(); + + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + if (!IsEnabled() || bPaintPlaceholderText) + rRenderContext.SetTextColor(rStyleSettings.GetDisableColor()); + + // Set background color of the normal text + if (mbForceControlBackground && IsControlBackground()) + { + // check if we need to set ControlBackground even in NWF case + rRenderContext.Push(vcl::PushFlags::FILLCOLOR | vcl::PushFlags::LINECOLOR); + rRenderContext.SetLineColor(); + rRenderContext.SetFillColor(GetControlBackground()); + rRenderContext.DrawRect(tools::Rectangle(aPos, Size(GetOutputSizePixel().Width() - 2 * mnXOffset, GetOutputSizePixel().Height()))); + rRenderContext.Pop(); + + rRenderContext.SetTextFillColor(GetControlBackground()); + } + else if (IsPaintTransparent() || ImplUseNativeBorder(rRenderContext, GetStyle())) + rRenderContext.SetTextFillColor(); + else + rRenderContext.SetTextFillColor(IsControlBackground() ? GetControlBackground() : rStyleSettings.GetFieldColor()); + + ImplPaintBorder(rRenderContext); + + bool bDrawSelection = maSelection.Len() && (HasFocus() || (GetStyle() & WB_NOHIDESELECTION) || mbActivePopup); + + aPos.setX( mnXOffset + ImplGetExtraXOffset() ); + if (bPaintPlaceholderText) + { + rRenderContext.DrawText(aPos, maPlaceholderText); + } + else if (!bDrawSelection && !mpIMEInfos) + { + rRenderContext.DrawText(aPos, aText, 0, nLen); + } + else + { + // save graphics state + rRenderContext.Push(); + // first calculate highlighted and non highlighted clip regions + vcl::Region aHighlightClipRegion; + vcl::Region aNormalClipRegion; + Selection aTmpSel(maSelection); + aTmpSel.Justify(); + // selection is highlighted + for(sal_Int32 i = 0; i < nLen; ++i) + { + tools::Rectangle aRect(aPos, Size(10, nTH)); + aRect.SetLeft( pDX[2 * i] + mnXOffset + ImplGetExtraXOffset() ); + aRect.SetRight( pDX[2 * i + 1] + mnXOffset + ImplGetExtraXOffset() ); + aRect.Justify(); + bool bHighlight = false; + if (i >= aTmpSel.Min() && i < aTmpSel.Max()) + bHighlight = true; + + if (mpIMEInfos && mpIMEInfos->pAttribs && + i >= mpIMEInfos->nPos && i < (mpIMEInfos->nPos+mpIMEInfos->nLen) && + (mpIMEInfos->pAttribs[i - mpIMEInfos->nPos] & ExtTextInputAttr::Highlight)) + { + bHighlight = true; + } + + if (bHighlight) + aHighlightClipRegion.Union(aRect); + else + aNormalClipRegion.Union(aRect); + } + // draw normal text + Color aNormalTextColor = rRenderContext.GetTextColor(); + rRenderContext.SetClipRegion(aNormalClipRegion); + + if (IsPaintTransparent()) + rRenderContext.SetTextFillColor(); + else + { + // Set background color when part of the text is selected + if (ImplUseNativeBorder(rRenderContext, GetStyle())) + { + if( mbForceControlBackground && IsControlBackground() ) + rRenderContext.SetTextFillColor(GetControlBackground()); + else + rRenderContext.SetTextFillColor(); + } + else + { + rRenderContext.SetTextFillColor(IsControlBackground() ? GetControlBackground() : rStyleSettings.GetFieldColor()); + } + } + rRenderContext.DrawText(aPos, aText, 0, nLen); + + // draw highlighted text + rRenderContext.SetClipRegion(aHighlightClipRegion); + rRenderContext.SetTextColor(rStyleSettings.GetHighlightTextColor()); + rRenderContext.SetTextFillColor(rStyleSettings.GetHighlightColor()); + rRenderContext.DrawText(aPos, aText, 0, nLen); + + // if IME info exists loop over portions and output different font attributes + if (mpIMEInfos && mpIMEInfos->pAttribs) + { + for(int n = 0; n < 2; n++) + { + vcl::Region aRegion; + if (n == 0) + { + rRenderContext.SetTextColor(aNormalTextColor); + if (IsPaintTransparent()) + rRenderContext.SetTextFillColor(); + else + rRenderContext.SetTextFillColor(IsControlBackground() ? GetControlBackground() : rStyleSettings.GetFieldColor()); + aRegion = aNormalClipRegion; + } + else + { + rRenderContext.SetTextColor(rStyleSettings.GetHighlightTextColor()); + rRenderContext.SetTextFillColor(rStyleSettings.GetHighlightColor()); + aRegion = aHighlightClipRegion; + } + + for(int i = 0; i < mpIMEInfos->nLen; ) + { + ExtTextInputAttr nAttr = mpIMEInfos->pAttribs[i]; + vcl::Region aClip; + int nIndex = i; + while (nIndex < mpIMEInfos->nLen && mpIMEInfos->pAttribs[nIndex] == nAttr) // #112631# check nIndex before using it + { + tools::Rectangle aRect( aPos, Size( 10, nTH ) ); + aRect.SetLeft( pDX[2 * (nIndex + mpIMEInfos->nPos)] + mnXOffset + ImplGetExtraXOffset() ); + aRect.SetRight( pDX[2 * (nIndex + mpIMEInfos->nPos) + 1] + mnXOffset + ImplGetExtraXOffset() ); + aRect.Justify(); + aClip.Union(aRect); + nIndex++; + } + i = nIndex; + aClip.Intersect(aRegion); + if (!aClip.IsEmpty() && nAttr != ExtTextInputAttr::NONE) + { + vcl::Font aFont = rRenderContext.GetFont(); + if (nAttr & ExtTextInputAttr::Underline) + aFont.SetUnderline(LINESTYLE_SINGLE); + else if (nAttr & ExtTextInputAttr::DoubleUnderline) + aFont.SetUnderline(LINESTYLE_DOUBLE); + else if (nAttr & ExtTextInputAttr::BoldUnderline) + aFont.SetUnderline( LINESTYLE_BOLD); + else if (nAttr & ExtTextInputAttr::DottedUnderline) + aFont.SetUnderline( LINESTYLE_DOTTED); + else if (nAttr & ExtTextInputAttr::DashDotUnderline) + aFont.SetUnderline( LINESTYLE_DASHDOT); + else if (nAttr & ExtTextInputAttr::GrayWaveline) + { + aFont.SetUnderline(LINESTYLE_WAVE); + rRenderContext.SetTextLineColor(COL_LIGHTGRAY); + } + rRenderContext.SetFont(aFont); + + if (nAttr & ExtTextInputAttr::RedText) + rRenderContext.SetTextColor(COL_RED); + else if (nAttr & ExtTextInputAttr::HalfToneText) + rRenderContext.SetTextColor(COL_LIGHTGRAY); + + rRenderContext.SetClipRegion(aClip); + rRenderContext.DrawText(aPos, aText, 0, nLen); + } + } + } + } + + // restore graphics state + rRenderContext.Pop(); + } + + if (bVisCursor && (!mpIMEInfos || mpIMEInfos->bCursor)) + pCursor->Show(); +} + +void Edit::ImplDelete( const Selection& rSelection, sal_uInt8 nDirection, sal_uInt8 nMode ) +{ + const sal_Int32 nTextLen = ImplGetText().getLength(); + + // deleting possible? + if ( !rSelection.Len() && + (((rSelection.Min() == 0) && (nDirection == EDIT_DEL_LEFT)) || + ((rSelection.Max() == nTextLen) && (nDirection == EDIT_DEL_RIGHT))) ) + return; + + ImplClearLayoutData(); + + Selection aSelection( rSelection ); + aSelection.Justify(); + + if ( !aSelection.Len() ) + { + uno::Reference < i18n::XBreakIterator > xBI = ImplGetBreakIterator(); + if ( nDirection == EDIT_DEL_LEFT ) + { + if ( nMode == EDIT_DELMODE_RESTOFWORD ) + { + const OUString sText = maText.toString(); + i18n::Boundary aBoundary = xBI->getWordBoundary( sText, aSelection.Min(), + GetSettings().GetLanguageTag().getLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES, true ); + auto startPos = aBoundary.startPos; + if ( startPos == aSelection.Min() ) + { + aBoundary = xBI->previousWord( sText, aSelection.Min(), + GetSettings().GetLanguageTag().getLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES ); + startPos = std::max(aBoundary.startPos, sal_Int32(0)); + } + aSelection.Min() = startPos; + } + else if ( nMode == EDIT_DELMODE_RESTOFCONTENT ) + { + aSelection.Min() = 0; + } + else + { + sal_Int32 nCount = 1; + aSelection.Min() = xBI->previousCharacters( maText.toString(), aSelection.Min(), + GetSettings().GetLanguageTag().getLocale(), i18n::CharacterIteratorMode::SKIPCHARACTER, nCount, nCount ); + } + } + else + { + if ( nMode == EDIT_DELMODE_RESTOFWORD ) + { + i18n::Boundary aBoundary = xBI->nextWord( maText.toString(), aSelection.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES ); + aSelection.Max() = aBoundary.startPos; + } + else if ( nMode == EDIT_DELMODE_RESTOFCONTENT ) + { + aSelection.Max() = nTextLen; + } + else + { + sal_Int32 nCount = 1; + aSelection.Max() = xBI->nextCharacters( maText.toString(), aSelection.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::CharacterIteratorMode::SKIPCHARACTER, nCount, nCount ); + } + } + } + + const auto nSelectionMin = aSelection.Min(); + maText.remove( nSelectionMin, aSelection.Len() ); + maSelection.Min() = nSelectionMin; + maSelection.Max() = nSelectionMin; + ImplAlignAndPaint(); + mbInternModified = true; +} + +OUString Edit::ImplGetValidString( const OUString& rString ) +{ + OUString aValidString = rString.replaceAll("\n", "").replaceAll("\r", ""); + aValidString = aValidString.replace('\t', ' '); + return aValidString; +} + +uno::Reference <i18n::XBreakIterator> const& Edit::ImplGetBreakIterator() +{ + if (!mxBreakIterator) + mxBreakIterator = i18n::BreakIterator::create(::comphelper::getProcessComponentContext()); + return mxBreakIterator; +} + +uno::Reference <i18n::XExtendedInputSequenceChecker> const& Edit::ImplGetInputSequenceChecker() +{ + if (!mxISC.is()) + mxISC = i18n::InputSequenceChecker::create(::comphelper::getProcessComponentContext()); + return mxISC; +} + +void Edit::ShowTruncationWarning(weld::Widget* pParent) +{ + std::unique_ptr<weld::MessageDialog> xBox(Application::CreateMessageDialog(pParent, VclMessageType::Warning, + VclButtonsType::Ok, VclResId(SV_EDIT_WARNING_STR))); + xBox->run(); +} + +bool Edit::ImplTruncateToMaxLen( OUString& rStr, sal_Int32 nSelectionLen ) const +{ + bool bWasTruncated = false; + if (maText.getLength() - nSelectionLen > mnMaxTextLen - rStr.getLength()) + { + sal_Int32 nErasePos = mnMaxTextLen - maText.getLength() + nSelectionLen; + rStr = rStr.copy( 0, nErasePos ); + bWasTruncated = true; + } + return bWasTruncated; +} + +void Edit::ImplInsertText( const OUString& rStr, const Selection* pNewSel, bool bIsUserInput ) +{ + Selection aSelection( maSelection ); + aSelection.Justify(); + + OUString aNewText( ImplGetValidString( rStr ) ); + + // as below, if there's no selection, but we're in overwrite mode and not beyond + // the end of the existing text then that's like a selection of 1 + auto nSelectionLen = aSelection.Len(); + if (!nSelectionLen && !mbInsertMode && aSelection.Max() < maText.getLength()) + nSelectionLen = 1; + ImplTruncateToMaxLen( aNewText, nSelectionLen ); + + ImplClearLayoutData(); + + if ( aSelection.Len() ) + maText.remove( aSelection.Min(), aSelection.Len() ); + else if (!mbInsertMode && aSelection.Max() < maText.getLength()) + maText.remove( aSelection.Max(), 1 ); + + // take care of input-sequence-checking now + if (bIsUserInput && !rStr.isEmpty()) + { + SAL_WARN_IF( rStr.getLength() != 1, "vcl", "unexpected string length. User input is expected to provide 1 char only!" ); + + // determine if input-sequence-checking should be applied or not + + uno::Reference < i18n::XBreakIterator > xBI = ImplGetBreakIterator(); + bool bIsInputSequenceChecking = rStr.getLength() == 1 && + officecfg::Office::Common::I18N::CTL::CTLFont::get() && + officecfg::Office::Common::I18N::CTL::CTLSequenceChecking::get() && + aSelection.Min() > 0 && /* first char needs not to be checked */ + xBI.is() && i18n::ScriptType::COMPLEX == xBI->getScriptType( rStr, 0 ); + + if (bIsInputSequenceChecking) + { + uno::Reference < i18n::XExtendedInputSequenceChecker > xISC = ImplGetInputSequenceChecker(); + if (xISC.is()) + { + sal_Unicode cChar = rStr[0]; + sal_Int32 nTmpPos = aSelection.Min(); + sal_Int16 nCheckMode = officecfg::Office::Common::I18N::CTL::CTLSequenceCheckingRestricted::get()? + i18n::InputSequenceCheckMode::STRICT : i18n::InputSequenceCheckMode::BASIC; + + // the text that needs to be checked is only the one + // before the current cursor position + const OUString aOldText( maText.getStr(), nTmpPos); + OUString aTmpText( aOldText ); + if (officecfg::Office::Common::I18N::CTL::CTLSequenceCheckingTypeAndReplace::get()) + { + xISC->correctInputSequence( aTmpText, nTmpPos - 1, cChar, nCheckMode ); + + // find position of first character that has changed + sal_Int32 nOldLen = aOldText.getLength(); + sal_Int32 nTmpLen = aTmpText.getLength(); + const sal_Unicode *pOldTxt = aOldText.getStr(); + const sal_Unicode *pTmpTxt = aTmpText.getStr(); + sal_Int32 nChgPos = 0; + while ( nChgPos < nOldLen && nChgPos < nTmpLen && + pOldTxt[nChgPos] == pTmpTxt[nChgPos] ) + ++nChgPos; + + const OUString aChgText( aTmpText.copy( nChgPos ) ); + + // remove text from first pos to be changed to current pos + maText.remove( nChgPos, nTmpPos - nChgPos ); + + if (!aChgText.isEmpty()) + { + aNewText = aChgText; + aSelection.Min() = nChgPos; // position for new text to be inserted + } + else + aNewText.clear(); + } + else + { + // should the character be ignored (i.e. not get inserted) ? + if (!xISC->checkInputSequence( aOldText, nTmpPos - 1, cChar, nCheckMode )) + aNewText.clear(); + } + } + } + + // at this point now we will insert the non-empty text 'normally' some lines below... + } + + if ( !aNewText.isEmpty() ) + maText.insert( aSelection.Min(), aNewText ); + + if ( !pNewSel ) + { + maSelection.Min() = aSelection.Min() + aNewText.getLength(); + maSelection.Max() = maSelection.Min(); + } + else + { + maSelection = *pNewSel; + if ( maSelection.Min() > maText.getLength() ) + maSelection.Min() = maText.getLength(); + if ( maSelection.Max() > maText.getLength() ) + maSelection.Max() = maText.getLength(); + } + + ImplAlignAndPaint(); + mbInternModified = true; +} + +void Edit::ImplSetText( const OUString& rText, const Selection* pNewSelection ) +{ + // we delete text by "selecting" the old text completely then calling InsertText; this is flicker free + if ( ( rText.getLength() > mnMaxTextLen ) || + ( std::u16string_view(rText) == std::u16string_view(maText.getStr(), maText.getLength()) + && (!pNewSelection || (*pNewSelection == maSelection)) ) ) + return; + + ImplClearLayoutData(); + maSelection.Min() = 0; + maSelection.Max() = maText.getLength(); + if ( mnXOffset || HasPaintEvent() ) + { + mnXOffset = 0; + maText = ImplGetValidString( rText ); + + // #i54929# recalculate mnXOffset before ImplSetSelection, + // else cursor ends up in wrong position + ImplAlign(); + + if ( pNewSelection ) + ImplSetSelection( *pNewSelection, false ); + + if ( mnXOffset && !pNewSelection ) + maSelection.Max() = 0; + + Invalidate(); + } + else + ImplInsertText( rText, pNewSelection ); + + CallEventListeners( VclEventId::EditModify ); +} + +ControlType Edit::ImplGetNativeControlType() const +{ + ControlType nCtrl = ControlType::Generic; + const vcl::Window* pControl = mbIsSubEdit ? GetParent() : this; + + switch (pControl->GetType()) + { + case WindowType::COMBOBOX: + case WindowType::PATTERNBOX: + case WindowType::NUMERICBOX: + case WindowType::METRICBOX: + case WindowType::CURRENCYBOX: + case WindowType::DATEBOX: + case WindowType::TIMEBOX: + case WindowType::LONGCURRENCYBOX: + nCtrl = ControlType::Combobox; + break; + + case WindowType::MULTILINEEDIT: + if ( GetWindow( GetWindowType::Border ) != this ) + nCtrl = ControlType::MultilineEditbox; + else + nCtrl = ControlType::EditboxNoBorder; + break; + + case WindowType::EDIT: + case WindowType::PATTERNFIELD: + case WindowType::METRICFIELD: + case WindowType::CURRENCYFIELD: + case WindowType::DATEFIELD: + case WindowType::TIMEFIELD: + case WindowType::SPINFIELD: + case WindowType::FORMATTEDFIELD: + if (pControl->GetStyle() & WB_SPIN) + nCtrl = ControlType::Spinbox; + else + { + if (GetWindow(GetWindowType::Border) != this) + nCtrl = ControlType::Editbox; + else + nCtrl = ControlType::EditboxNoBorder; + } + break; + + default: + nCtrl = ControlType::Editbox; + } + return nCtrl; +} + +void Edit::ImplClearBackground(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRectangle, tools::Long nXStart, tools::Long nXEnd ) +{ + /* + * note: at this point the cursor must be switched off already + */ + tools::Rectangle aRect(Point(), GetOutputSizePixel()); + aRect.SetLeft( nXStart ); + aRect.SetRight( nXEnd ); + + if( !(ImplUseNativeBorder(rRenderContext, GetStyle()) || IsPaintTransparent())) + rRenderContext.Erase(aRect); + else if (SupportsDoubleBuffering() && mbIsSubEdit) + { + // ImplPaintBorder() is a NOP, we have a native border, and this is a sub-edit of a control. + // That means we have to draw the parent native widget to paint the edit area to clear our background. + vcl::PaintBufferGuard g(ImplGetWindowImpl()->mpFrameData, GetParent()); + GetParent()->Paint(rRenderContext, rRectangle); + } +} + +void Edit::ImplPaintBorder(vcl::RenderContext const & rRenderContext) +{ + // this is not needed when double-buffering + if (SupportsDoubleBuffering()) + return; + + if (!(ImplUseNativeBorder(rRenderContext, GetStyle()) || IsPaintTransparent())) + return; + + // draw the inner part by painting the whole control using its border window + vcl::Window* pBorder = GetWindow(GetWindowType::Border); + if (pBorder == this) + { + // we have no border, use parent + vcl::Window* pControl = mbIsSubEdit ? GetParent() : this; + pBorder = pControl->GetWindow(GetWindowType::Border); + if (pBorder == this) + pBorder = GetParent(); + } + + if (!pBorder) + return; + + // set proper clipping region to not overdraw the whole control + vcl::Region aClipRgn = GetPaintRegion(); + if (!aClipRgn.IsNull()) + { + // transform clipping region to border window's coordinate system + if (IsRTLEnabled() != pBorder->IsRTLEnabled() && AllSettings::GetLayoutRTL()) + { + // need to mirror in case border is not RTL but edit is (or vice versa) + + // mirror + tools::Rectangle aBounds(aClipRgn.GetBoundRect()); + int xNew = GetOutputSizePixel().Width() - aBounds.GetWidth() - aBounds.Left(); + aClipRgn.Move(xNew - aBounds.Left(), 0); + + // move offset of border window + Point aBorderOffs = pBorder->ScreenToOutputPixel(OutputToScreenPixel(Point())); + aClipRgn.Move(aBorderOffs.X(), aBorderOffs.Y()); + } + else + { + // normal case + Point aBorderOffs = pBorder->ScreenToOutputPixel(OutputToScreenPixel(Point())); + aClipRgn.Move(aBorderOffs.X(), aBorderOffs.Y()); + } + + vcl::Region oldRgn(pBorder->GetOutDev()->GetClipRegion()); + pBorder->GetOutDev()->SetClipRegion(aClipRgn); + + pBorder->Paint(*pBorder->GetOutDev(), tools::Rectangle()); + + pBorder->GetOutDev()->SetClipRegion(oldRgn); + } + else + { + pBorder->Paint(*pBorder->GetOutDev(), tools::Rectangle()); + } +} + +void Edit::ImplShowCursor( bool bOnlyIfVisible ) +{ + if ( !IsUpdateMode() || ( bOnlyIfVisible && !IsReallyVisible() ) ) + return; + + vcl::Cursor* pCursor = GetCursor(); + OUString aText = ImplGetText(); + + tools::Long nTextPos = 0; + + sal_Int32 nDXBuffer[256]; + std::unique_ptr<sal_Int32[]> pDXBuffer; + sal_Int32* pDX = nDXBuffer; + + if( !aText.isEmpty() ) + { + if( o3tl::make_unsigned(2*aText.getLength()) > SAL_N_ELEMENTS(nDXBuffer) ) + { + pDXBuffer.reset(new sal_Int32[2*(aText.getLength()+1)]); + pDX = pDXBuffer.get(); + } + + GetOutDev()->GetCaretPositions( aText, pDX, 0, aText.getLength() ); + + if( maSelection.Max() < aText.getLength() ) + nTextPos = pDX[ 2*maSelection.Max() ]; + else + nTextPos = pDX[ 2*aText.getLength()-1 ]; + } + + tools::Long nCursorWidth = 0; + if ( !mbInsertMode && !maSelection.Len() && (maSelection.Max() < aText.getLength()) ) + nCursorWidth = GetTextWidth(aText, maSelection.Max(), 1); + tools::Long nCursorPosX = nTextPos + mnXOffset + ImplGetExtraXOffset(); + + // cursor should land in visible area + const Size aOutSize = GetOutputSizePixel(); + if ( (nCursorPosX < 0) || (nCursorPosX >= aOutSize.Width()) ) + { + tools::Long nOldXOffset = mnXOffset; + + if ( nCursorPosX < 0 ) + { + mnXOffset = - nTextPos; + tools::Long nMaxX = 0; + mnXOffset += aOutSize.Width() / 5; + if ( mnXOffset > nMaxX ) + mnXOffset = nMaxX; + } + else + { + mnXOffset = (aOutSize.Width()-ImplGetExtraXOffset()) - nTextPos; + // Something more? + if ( (aOutSize.Width()-ImplGetExtraXOffset()) < nTextPos ) + { + tools::Long nMaxNegX = (aOutSize.Width()-ImplGetExtraXOffset()) - GetTextWidth( aText ); + mnXOffset -= aOutSize.Width() / 5; + if ( mnXOffset < nMaxNegX ) // both negative... + mnXOffset = nMaxNegX; + } + } + + nCursorPosX = nTextPos + mnXOffset + ImplGetExtraXOffset(); + if ( nCursorPosX == aOutSize.Width() ) // then invisible... + nCursorPosX--; + + if ( mnXOffset != nOldXOffset ) + ImplInvalidateOrRepaint(); + } + + const tools::Long nTextHeight = GetTextHeight(); + const tools::Long nCursorPosY = ImplGetTextYPosition(); + if (pCursor) + { + pCursor->SetPos( Point( nCursorPosX, nCursorPosY ) ); + pCursor->SetSize( Size( nCursorWidth, nTextHeight ) ); + pCursor->Show(); + } +} + +void Edit::ImplAlign() +{ + if (mnAlign == EDIT_ALIGN_LEFT && !mnXOffset) + { + // short circuit common case and avoid slow GetTextWidth() calc + return; + } + + tools::Long nTextWidth = GetTextWidth( ImplGetText() ); + tools::Long nOutWidth = GetOutputSizePixel().Width(); + + if ( mnAlign == EDIT_ALIGN_LEFT ) + { + if (nTextWidth < nOutWidth) + mnXOffset = 0; + } + else if ( mnAlign == EDIT_ALIGN_RIGHT ) + { + tools::Long nMinXOffset = nOutWidth - nTextWidth - 1 - ImplGetExtraXOffset(); + bool bRTL = IsRTLEnabled(); + if( mbIsSubEdit && GetParent() ) + bRTL = GetParent()->IsRTLEnabled(); + if( bRTL ) + { + if( nTextWidth < nOutWidth ) + mnXOffset = nMinXOffset; + } + else + { + if( nTextWidth < nOutWidth ) + mnXOffset = nMinXOffset; + else if ( mnXOffset < nMinXOffset ) + mnXOffset = nMinXOffset; + } + } + else if( mnAlign == EDIT_ALIGN_CENTER ) + { + // would be nicer with check while scrolling but then it's not centred in scrolled state + mnXOffset = (nOutWidth - nTextWidth) / 2; + } +} + +void Edit::ImplAlignAndPaint() +{ + ImplAlign(); + ImplInvalidateOrRepaint(); + ImplShowCursor(); +} + +sal_Int32 Edit::ImplGetCharPos( const Point& rWindowPos ) const +{ + sal_Int32 nIndex = EDIT_NOLIMIT; + OUString aText = ImplGetText(); + + sal_Int32 nDXBuffer[256]; + std::unique_ptr<sal_Int32[]> pDXBuffer; + sal_Int32* pDX = nDXBuffer; + if( o3tl::make_unsigned(2*aText.getLength()) > SAL_N_ELEMENTS(nDXBuffer) ) + { + pDXBuffer.reset(new sal_Int32[2*(aText.getLength()+1)]); + pDX = pDXBuffer.get(); + } + + GetOutDev()->GetCaretPositions( aText, pDX, 0, aText.getLength() ); + tools::Long nX = rWindowPos.X() - mnXOffset - ImplGetExtraXOffset(); + for (sal_Int32 i = 0; i < aText.getLength(); aText.iterateCodePoints(&i)) + { + if( (pDX[2*i] >= nX && pDX[2*i+1] <= nX) || + (pDX[2*i+1] >= nX && pDX[2*i] <= nX)) + { + nIndex = i; + if( pDX[2*i] < pDX[2*i+1] ) + { + if( nX > (pDX[2*i]+pDX[2*i+1])/2 ) + aText.iterateCodePoints(&nIndex); + } + else + { + if( nX < (pDX[2*i]+pDX[2*i+1])/2 ) + aText.iterateCodePoints(&nIndex); + } + break; + } + } + if( nIndex == EDIT_NOLIMIT ) + { + nIndex = 0; + sal_Int32 nFinalIndex = 0; + tools::Long nDiff = std::abs( pDX[0]-nX ); + sal_Int32 i = 0; + if (!aText.isEmpty()) + { + aText.iterateCodePoints(&i); //skip the first character + } + while (i < aText.getLength()) + { + tools::Long nNewDiff = std::abs( pDX[2*i]-nX ); + + if( nNewDiff < nDiff ) + { + nIndex = i; + nDiff = nNewDiff; + } + + nFinalIndex = i; + + aText.iterateCodePoints(&i); + } + if (nIndex == nFinalIndex && std::abs( pDX[2*nIndex+1] - nX ) < nDiff) + nIndex = EDIT_NOLIMIT; + } + + return nIndex; +} + +void Edit::ImplSetCursorPos( sal_Int32 nChar, bool bSelect ) +{ + Selection aSelection( maSelection ); + aSelection.Max() = nChar; + if ( !bSelect ) + aSelection.Min() = aSelection.Max(); + ImplSetSelection( aSelection ); +} + +void Edit::ImplCopyToSelectionClipboard() +{ + if ( GetSelection().Len() ) + { + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aSelection(GetSystemPrimarySelection()); + ImplCopy( aSelection ); + } +} + +void Edit::ImplCopy( uno::Reference< datatransfer::clipboard::XClipboard > const & rxClipboard ) +{ + vcl::unohelper::TextDataObject::CopyStringTo( GetSelected(), rxClipboard ); +} + +void Edit::ImplPaste( uno::Reference< datatransfer::clipboard::XClipboard > const & rxClipboard ) +{ + if ( !rxClipboard.is() ) + return; + + uno::Reference< datatransfer::XTransferable > xDataObj; + + try + { + SolarMutexReleaser aReleaser; + xDataObj = rxClipboard->getContents(); + } + catch( const css::uno::Exception& ) + { + } + + if ( !xDataObj.is() ) + return; + + datatransfer::DataFlavor aFlavor; + SotExchange::GetFormatDataFlavor( SotClipboardFormatId::STRING, aFlavor ); + try + { + uno::Any aData = xDataObj->getTransferData( aFlavor ); + OUString aText; + aData >>= aText; + + Selection aSelection(maSelection); + aSelection.Justify(); + if (ImplTruncateToMaxLen(aText, aSelection.Len())) + ShowTruncationWarning(GetFrameWeld()); + + ReplaceSelected( aText ); + } + catch( const css::uno::Exception& ) + { + } +} + +void Edit::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if ( mpSubEdit ) + { + Control::MouseButtonDown( rMEvt ); + return; + } + + sal_Int32 nCharPos = ImplGetCharPos( rMEvt.GetPosPixel() ); + Selection aSelection( maSelection ); + aSelection.Justify(); + + if ( rMEvt.GetClicks() < 4 ) + { + mbClickedInSelection = false; + if ( rMEvt.GetClicks() == 3 ) + { + ImplSetSelection( Selection( 0, EDIT_NOLIMIT) ); + ImplCopyToSelectionClipboard(); + + } + else if ( rMEvt.GetClicks() == 2 ) + { + uno::Reference < i18n::XBreakIterator > xBI = ImplGetBreakIterator(); + i18n::Boundary aBoundary = xBI->getWordBoundary( maText.toString(), aSelection.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES, true ); + ImplSetSelection( Selection( aBoundary.startPos, aBoundary.endPos ) ); + ImplCopyToSelectionClipboard(); + } + else if ( !rMEvt.IsShift() && HasFocus() && aSelection.Contains( nCharPos ) ) + mbClickedInSelection = true; + else if ( rMEvt.IsLeft() ) + ImplSetCursorPos( nCharPos, rMEvt.IsShift() ); + + if ( !mbClickedInSelection && rMEvt.IsLeft() && ( rMEvt.GetClicks() == 1 ) ) + StartTracking( StartTrackingFlags::ScrollRepeat ); + } + + GrabFocus(); +} + +void Edit::MouseButtonUp( const MouseEvent& rMEvt ) +{ + if ( mbClickedInSelection && rMEvt.IsLeft() ) + { + sal_Int32 nCharPos = ImplGetCharPos( rMEvt.GetPosPixel() ); + ImplSetCursorPos( nCharPos, false ); + mbClickedInSelection = false; + } + else if ( rMEvt.IsMiddle() && !mbReadOnly && + ( GetSettings().GetMouseSettings().GetMiddleButtonAction() == MouseMiddleButtonAction::PasteSelection ) ) + { + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aSelection(GetSystemPrimarySelection()); + ImplPaste( aSelection ); + Modify(); + } +} + +void Edit::Tracking( const TrackingEvent& rTEvt ) +{ + if ( rTEvt.IsTrackingEnded() ) + { + if ( mbClickedInSelection ) + { + sal_Int32 nCharPos = ImplGetCharPos( rTEvt.GetMouseEvent().GetPosPixel() ); + ImplSetCursorPos( nCharPos, false ); + mbClickedInSelection = false; + } + else if ( rTEvt.GetMouseEvent().IsLeft() ) + { + ImplCopyToSelectionClipboard(); + } + } + else + { + if( !mbClickedInSelection ) + { + sal_Int32 nCharPos = ImplGetCharPos( rTEvt.GetMouseEvent().GetPosPixel() ); + ImplSetCursorPos( nCharPos, true ); + } + } +} + +bool Edit::ImplHandleKeyEvent( const KeyEvent& rKEvt ) +{ + bool bDone = false; + sal_uInt16 nCode = rKEvt.GetKeyCode().GetCode(); + KeyFuncType eFunc = rKEvt.GetKeyCode().GetFunction(); + + mbInternModified = false; + + if ( eFunc != KeyFuncType::DONTKNOW ) + { + switch ( eFunc ) + { + case KeyFuncType::CUT: + { + if ( !mbReadOnly && maSelection.Len() && !mbPassword ) + { + Cut(); + Modify(); + bDone = true; + } + } + break; + + case KeyFuncType::COPY: + { + if ( !mbPassword ) + { + Copy(); + bDone = true; + } + } + break; + + case KeyFuncType::PASTE: + { + if ( !mbReadOnly ) + { + Paste(); + bDone = true; + } + } + break; + + case KeyFuncType::UNDO: + { + if ( !mbReadOnly ) + { + Undo(); + bDone = true; + } + } + break; + + default: + eFunc = KeyFuncType::DONTKNOW; + } + } + + if ( !bDone && rKEvt.GetKeyCode().IsMod1() && !rKEvt.GetKeyCode().IsMod2() ) + { + if ( nCode == KEY_A ) + { + ImplSetSelection( Selection( 0, maText.getLength() ) ); + bDone = true; + } + else if ( rKEvt.GetKeyCode().IsShift() && (nCode == KEY_S) ) + { + if ( pImplFncGetSpecialChars ) + { + Selection aSaveSel = GetSelection(); // if someone changes the selection in Get/LoseFocus, e.g. URL bar + OUString aChars = pImplFncGetSpecialChars( GetFrameWeld(), GetFont() ); + SetSelection( aSaveSel ); + if ( !aChars.isEmpty() ) + { + ImplInsertText( aChars ); + Modify(); + } + bDone = true; + } + } + } + + if ( eFunc == KeyFuncType::DONTKNOW && ! bDone ) + { + switch ( nCode ) + { + case css::awt::Key::SELECT_ALL: + { + ImplSetSelection( Selection( 0, maText.getLength() ) ); + bDone = true; + } + break; + + case KEY_LEFT: + case KEY_RIGHT: + case KEY_HOME: + case KEY_END: + case css::awt::Key::MOVE_WORD_FORWARD: + case css::awt::Key::SELECT_WORD_FORWARD: + case css::awt::Key::MOVE_WORD_BACKWARD: + case css::awt::Key::SELECT_WORD_BACKWARD: + case css::awt::Key::MOVE_TO_BEGIN_OF_LINE: + case css::awt::Key::MOVE_TO_END_OF_LINE: + case css::awt::Key::SELECT_TO_BEGIN_OF_LINE: + case css::awt::Key::SELECT_TO_END_OF_LINE: + case css::awt::Key::MOVE_TO_BEGIN_OF_PARAGRAPH: + case css::awt::Key::MOVE_TO_END_OF_PARAGRAPH: + case css::awt::Key::SELECT_TO_BEGIN_OF_PARAGRAPH: + case css::awt::Key::SELECT_TO_END_OF_PARAGRAPH: + case css::awt::Key::MOVE_TO_BEGIN_OF_DOCUMENT: + case css::awt::Key::MOVE_TO_END_OF_DOCUMENT: + case css::awt::Key::SELECT_TO_BEGIN_OF_DOCUMENT: + case css::awt::Key::SELECT_TO_END_OF_DOCUMENT: + { + if ( !rKEvt.GetKeyCode().IsMod2() ) + { + ImplClearLayoutData(); + uno::Reference < i18n::XBreakIterator > xBI = ImplGetBreakIterator(); + + Selection aSel( maSelection ); + bool bWord = rKEvt.GetKeyCode().IsMod1(); + bool bSelect = rKEvt.GetKeyCode().IsShift(); + bool bGoLeft = (nCode == KEY_LEFT); + bool bGoRight = (nCode == KEY_RIGHT); + bool bGoHome = (nCode == KEY_HOME); + bool bGoEnd = (nCode == KEY_END); + + switch( nCode ) + { + case css::awt::Key::MOVE_WORD_FORWARD: + bGoRight = bWord = true;break; + case css::awt::Key::SELECT_WORD_FORWARD: + bGoRight = bSelect = bWord = true;break; + case css::awt::Key::MOVE_WORD_BACKWARD: + bGoLeft = bWord = true;break; + case css::awt::Key::SELECT_WORD_BACKWARD: + bGoLeft = bSelect = bWord = true;break; + case css::awt::Key::SELECT_TO_BEGIN_OF_LINE: + case css::awt::Key::SELECT_TO_BEGIN_OF_PARAGRAPH: + case css::awt::Key::SELECT_TO_BEGIN_OF_DOCUMENT: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_BEGIN_OF_LINE: + case css::awt::Key::MOVE_TO_BEGIN_OF_PARAGRAPH: + case css::awt::Key::MOVE_TO_BEGIN_OF_DOCUMENT: + bGoHome = true;break; + case css::awt::Key::SELECT_TO_END_OF_LINE: + case css::awt::Key::SELECT_TO_END_OF_PARAGRAPH: + case css::awt::Key::SELECT_TO_END_OF_DOCUMENT: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_END_OF_LINE: + case css::awt::Key::MOVE_TO_END_OF_PARAGRAPH: + case css::awt::Key::MOVE_TO_END_OF_DOCUMENT: + bGoEnd = true;break; + default: + break; + } + + // range is checked in ImplSetSelection ... + if ( bGoLeft && aSel.Max() ) + { + if ( bWord ) + { + const OUString sText = maText.toString(); + i18n::Boundary aBoundary = xBI->getWordBoundary( sText, aSel.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES, true ); + if ( aBoundary.startPos == aSel.Max() ) + aBoundary = xBI->previousWord( sText, aSel.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES ); + aSel.Max() = aBoundary.startPos; + } + else + { + sal_Int32 nCount = 1; + aSel.Max() = xBI->previousCharacters( maText.toString(), aSel.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::CharacterIteratorMode::SKIPCHARACTER, nCount, nCount ); + } + } + else if ( bGoRight && ( aSel.Max() < maText.getLength() ) ) + { + if ( bWord ) + { + i18n::Boundary aBoundary = xBI->nextWord( maText.toString(), aSel.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES ); + aSel.Max() = aBoundary.startPos; + } + else + { + sal_Int32 nCount = 1; + aSel.Max() = xBI->nextCharacters( maText.toString(), aSel.Max(), + GetSettings().GetLanguageTag().getLocale(), i18n::CharacterIteratorMode::SKIPCHARACTER, nCount, nCount ); + } + } + else if ( bGoHome ) + { + aSel.Max() = 0; + } + else if ( bGoEnd ) + { + aSel.Max() = EDIT_NOLIMIT; + } + + if ( !bSelect ) + aSel.Min() = aSel.Max(); + + if ( aSel != GetSelection() ) + { + ImplSetSelection( aSel ); + ImplCopyToSelectionClipboard(); + } + + if (bGoEnd && maAutocompleteHdl.IsSet() && !rKEvt.GetKeyCode().GetModifier()) + { + if ( (maSelection.Min() == maSelection.Max()) && (maSelection.Min() == maText.getLength()) ) + { + maAutocompleteHdl.Call(*this); + } + } + + bDone = true; + } + } + break; + + case css::awt::Key::DELETE_WORD_BACKWARD: + case css::awt::Key::DELETE_WORD_FORWARD: + case css::awt::Key::DELETE_TO_BEGIN_OF_LINE: + case css::awt::Key::DELETE_TO_END_OF_LINE: + case KEY_BACKSPACE: + case KEY_DELETE: + { + if ( !mbReadOnly && !rKEvt.GetKeyCode().IsMod2() ) + { + sal_uInt8 nDel = (nCode == KEY_DELETE) ? EDIT_DEL_RIGHT : EDIT_DEL_LEFT; + sal_uInt8 nMode = rKEvt.GetKeyCode().IsMod1() ? EDIT_DELMODE_RESTOFWORD : EDIT_DELMODE_SIMPLE; + if ( (nMode == EDIT_DELMODE_RESTOFWORD) && rKEvt.GetKeyCode().IsShift() ) + nMode = EDIT_DELMODE_RESTOFCONTENT; + switch( nCode ) + { + case css::awt::Key::DELETE_WORD_BACKWARD: + nDel = EDIT_DEL_LEFT; + nMode = EDIT_DELMODE_RESTOFWORD; + break; + case css::awt::Key::DELETE_WORD_FORWARD: + nDel = EDIT_DEL_RIGHT; + nMode = EDIT_DELMODE_RESTOFWORD; + break; + case css::awt::Key::DELETE_TO_BEGIN_OF_LINE: + nDel = EDIT_DEL_LEFT; + nMode = EDIT_DELMODE_RESTOFCONTENT; + break; + case css::awt::Key::DELETE_TO_END_OF_LINE: + nDel = EDIT_DEL_RIGHT; + nMode = EDIT_DELMODE_RESTOFCONTENT; + break; + default: break; + } + sal_Int32 nOldLen = maText.getLength(); + ImplDelete( maSelection, nDel, nMode ); + if ( maText.getLength() != nOldLen ) + Modify(); + bDone = true; + } + } + break; + + case KEY_INSERT: + { + if ( !mpIMEInfos && !mbReadOnly && !rKEvt.GetKeyCode().IsMod2() ) + { + SetInsertMode( !mbInsertMode ); + bDone = true; + } + } + break; + + case KEY_RETURN: + if (maActivateHdl.IsSet() && !rKEvt.GetKeyCode().GetModifier()) + bDone = maActivateHdl.Call(*this); + break; + + default: + { + if ( IsCharInput( rKEvt ) ) + { + bDone = true; // read characters also when in ReadOnly + if ( !mbReadOnly ) + { + ImplInsertText(OUString(rKEvt.GetCharCode()), nullptr, true); + if (maAutocompleteHdl.IsSet()) + { + if ( (maSelection.Min() == maSelection.Max()) && (maSelection.Min() == maText.getLength()) ) + { + maAutocompleteHdl.Call(*this); + } + } + } + } + } + } + } + + if ( mbInternModified ) + Modify(); + + return bDone; +} + +void Edit::KeyInput( const KeyEvent& rKEvt ) +{ + if ( mpSubEdit || !ImplHandleKeyEvent( rKEvt ) ) + Control::KeyInput( rKEvt ); +} + +void Edit::FillLayoutData() const +{ + mxLayoutData.emplace(); + const_cast<Edit*>(this)->Invalidate(); +} + +void Edit::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRectangle) +{ + if (!mpSubEdit) + ImplRepaint(rRenderContext, rRectangle); +} + +void Edit::Resize() +{ + if ( !mpSubEdit && IsReallyVisible() ) + { + Control::Resize(); + // because of vertical centering... + mnXOffset = 0; + ImplAlign(); + Invalidate(); + ImplShowCursor(); + } +} + +void Edit::Draw( OutputDevice* pDev, const Point& rPos, SystemTextColorFlags nFlags ) +{ + ApplySettings(*pDev); + + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + vcl::Font aFont = GetDrawPixelFont( pDev ); + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + pDev->SetTextFillColor(); + + // Border/Background + pDev->SetLineColor(); + pDev->SetFillColor(); + bool bBorder = (GetStyle() & WB_BORDER); + bool bBackground = IsControlBackground(); + if ( bBorder || bBackground ) + { + tools::Rectangle aRect( aPos, aSize ); + if ( bBorder ) + { + ImplDrawFrame( pDev, aRect ); + } + if ( bBackground ) + { + pDev->SetFillColor( GetControlBackground() ); + pDev->DrawRect( aRect ); + } + } + + // Content + if ( nFlags & SystemTextColorFlags::Mono ) + pDev->SetTextColor( COL_BLACK ); + else + { + if ( !IsEnabled() ) + { + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + pDev->SetTextColor( rStyleSettings.GetDisableColor() ); + } + else + { + pDev->SetTextColor( GetTextColor() ); + } + } + + const tools::Long nOnePixel = GetDrawPixel( pDev, 1 ); + const tools::Long nOffX = 3*nOnePixel; + DrawTextFlags nTextStyle = DrawTextFlags::VCenter; + tools::Rectangle aTextRect( aPos, aSize ); + + if ( GetStyle() & WB_CENTER ) + nTextStyle |= DrawTextFlags::Center; + else if ( GetStyle() & WB_RIGHT ) + nTextStyle |= DrawTextFlags::Right; + else + nTextStyle |= DrawTextFlags::Left; + + aTextRect.AdjustLeft(nOffX ); + aTextRect.AdjustRight( -nOffX ); + + OUString aText = ImplGetText(); + tools::Long nTextHeight = pDev->GetTextHeight(); + tools::Long nTextWidth = pDev->GetTextWidth( aText ); + tools::Long nOffY = (aSize.Height() - nTextHeight) / 2; + + // Clipping? + if ( (nOffY < 0) || + ((nOffY+nTextHeight) > aSize.Height()) || + ((nOffX+nTextWidth) > aSize.Width()) ) + { + tools::Rectangle aClip( aPos, aSize ); + if ( nTextHeight > aSize.Height() ) + aClip.AdjustBottom(nTextHeight-aSize.Height()+1 ); // prevent HP printers from 'optimizing' + pDev->IntersectClipRegion( aClip ); + } + + pDev->DrawText( aTextRect, aText, nTextStyle ); + pDev->Pop(); + + if ( GetSubEdit() ) + { + Size aOrigSize(GetSubEdit()->GetSizePixel()); + GetSubEdit()->SetSizePixel(GetSizePixel()); + GetSubEdit()->Draw(pDev, rPos, nFlags); + GetSubEdit()->SetSizePixel(aOrigSize); + } +} + +void Edit::ImplInvalidateOutermostBorder( vcl::Window* pWin ) +{ + // allow control to show focused state + vcl::Window *pInvalWin = pWin; + for (;;) + { + vcl::Window* pBorder = pInvalWin->GetWindow( GetWindowType::Border ); + if (pBorder == pInvalWin || !pBorder || + pInvalWin->ImplGetFrame() != pBorder->ImplGetFrame() ) + break; + pInvalWin = pBorder; + } + + pInvalWin->Invalidate( InvalidateFlags::Children | InvalidateFlags::Update ); +} + +void Edit::GetFocus() +{ + if ( mpSubEdit ) + mpSubEdit->ImplGrabFocus( GetGetFocusFlags() ); + else if ( !mbActivePopup ) + { + maUndoText = maText.toString(); + SelectionOptions nSelOptions = GetSettings().GetStyleSettings().GetSelectionOptions(); + if ( !( GetStyle() & (WB_NOHIDESELECTION|WB_READONLY) ) + && ( GetGetFocusFlags() & (GetFocusFlags::Init|GetFocusFlags::Tab|GetFocusFlags::CURSOR|GetFocusFlags::Mnemonic) ) ) + { + if ( nSelOptions & SelectionOptions::ShowFirst ) + { + maSelection.Min() = maText.getLength(); + maSelection.Max() = 0; + } + else + { + maSelection.Min() = 0; + maSelection.Max() = maText.getLength(); + } + if ( mbIsSubEdit ) + static_cast<Edit*>(GetParent())->CallEventListeners( VclEventId::EditSelectionChanged ); + else + CallEventListeners( VclEventId::EditSelectionChanged ); + } + + ImplShowCursor(); + + // FIXME: this is currently only on macOS + // check for other platforms that need similar handling + if( ImplGetSVData()->maNWFData.mbNoFocusRects && + IsNativeWidgetEnabled() && + IsNativeControlSupported( ControlType::Editbox, ControlPart::Entire ) ) + { + ImplInvalidateOutermostBorder( mbIsSubEdit ? GetParent() : this ); + } + else if ( maSelection.Len() ) + { + // paint the selection + if ( !HasPaintEvent() ) + ImplInvalidateOrRepaint(); + else + Invalidate(); + } + + SetInputContext( InputContext( GetFont(), !IsReadOnly() ? InputContextFlags::Text|InputContextFlags::ExtText : InputContextFlags::NONE ) ); + } + + Control::GetFocus(); +} + +void Edit::LoseFocus() +{ + if ( !mpSubEdit ) + { + // FIXME: this is currently only on macOS + // check for other platforms that need similar handling + if( ImplGetSVData()->maNWFData.mbNoFocusRects && + IsNativeWidgetEnabled() && + IsNativeControlSupported( ControlType::Editbox, ControlPart::Entire ) ) + { + ImplInvalidateOutermostBorder( mbIsSubEdit ? GetParent() : this ); + } + + if ( !mbActivePopup && !( GetStyle() & WB_NOHIDESELECTION ) && maSelection.Len() ) + ImplInvalidateOrRepaint(); // paint the selection + } + + Control::LoseFocus(); +} + +void Edit::Command( const CommandEvent& rCEvt ) +{ + if ( rCEvt.GetCommand() == CommandEventId::ContextMenu ) + { + VclPtr<PopupMenu> pPopup = Edit::CreatePopupMenu(); + + bool bEnableCut = true; + bool bEnableCopy = true; + bool bEnableDelete = true; + bool bEnablePaste = true; + bool bEnableSpecialChar = true; + + if ( !maSelection.Len() ) + { + bEnableCut = false; + bEnableCopy = false; + bEnableDelete = false; + } + + if ( IsReadOnly() ) + { + bEnableCut = false; + bEnablePaste = false; + bEnableDelete = false; + bEnableSpecialChar = false; + } + else + { + // only paste if text available in clipboard + bool bData = false; + uno::Reference< datatransfer::clipboard::XClipboard > xClipboard = GetClipboard(); + + if ( xClipboard.is() ) + { + uno::Reference< datatransfer::XTransferable > xDataObj; + { + SolarMutexReleaser aReleaser; + xDataObj = xClipboard->getContents(); + } + if ( xDataObj.is() ) + { + datatransfer::DataFlavor aFlavor; + SotExchange::GetFormatDataFlavor( SotClipboardFormatId::STRING, aFlavor ); + bData = xDataObj->isDataFlavorSupported( aFlavor ); + } + } + bEnablePaste = bData; + } + + pPopup->EnableItem(pPopup->GetItemId("cut"), bEnableCut); + pPopup->EnableItem(pPopup->GetItemId("copy"), bEnableCopy); + pPopup->EnableItem(pPopup->GetItemId("delete"), bEnableDelete); + pPopup->EnableItem(pPopup->GetItemId("paste"), bEnablePaste); + pPopup->EnableItem(pPopup->GetItemId("specialchar"), bEnableSpecialChar); + pPopup->EnableItem( + pPopup->GetItemId("undo"), + std::u16string_view(maUndoText) + != std::u16string_view(maText.getStr(), maText.getLength())); + bool bAllSelected = maSelection.Min() == 0 && maSelection.Max() == maText.getLength(); + pPopup->EnableItem(pPopup->GetItemId("selectall"), !bAllSelected); + pPopup->ShowItem(pPopup->GetItemId("specialchar"), pImplFncGetSpecialChars != nullptr); + + mbActivePopup = true; + Selection aSaveSel = GetSelection(); // if someone changes selection in Get/LoseFocus, e.g. URL bar + Point aPos = rCEvt.GetMousePosPixel(); + if ( !rCEvt.IsMouseEvent() ) + { + // Show menu eventually centered in selection + Size aSize = GetOutputSizePixel(); + aPos = Point( aSize.Width()/2, aSize.Height()/2 ); + } + sal_uInt16 n = pPopup->Execute( this, aPos ); + SetSelection( aSaveSel ); + OString sCommand = pPopup->GetItemIdent(n); + if (sCommand == "undo") + { + Undo(); + Modify(); + } + else if (sCommand == "cut") + { + Cut(); + Modify(); + } + else if (sCommand == "copy") + { + Copy(); + } + else if (sCommand == "paste") + { + Paste(); + Modify(); + } + else if (sCommand == "delete") + { + DeleteSelected(); + Modify(); + } + else if (sCommand == "selectall") + { + ImplSetSelection( Selection( 0, maText.getLength() ) ); + } + else if (sCommand == "specialchar" && pImplFncGetSpecialChars) + { + OUString aChars = pImplFncGetSpecialChars(GetFrameWeld(), GetFont()); + if (!isDisposed()) // destroyed while the insert special character dialog was still open + { + SetSelection( aSaveSel ); + if (!aChars.isEmpty()) + { + ImplInsertText( aChars ); + Modify(); + } + } + } + pPopup.clear(); + mbActivePopup = false; + } + else if ( rCEvt.GetCommand() == CommandEventId::StartExtTextInput ) + { + DeleteSelected(); + sal_Int32 nPos = maSelection.Max(); + mpIMEInfos.reset(new Impl_IMEInfos( nPos, maText.copy(nPos).makeStringAndClear() )); + mpIMEInfos->bWasCursorOverwrite = !IsInsertMode(); + } + else if ( rCEvt.GetCommand() == CommandEventId::EndExtTextInput ) + { + bool bInsertMode = !mpIMEInfos->bWasCursorOverwrite; + mpIMEInfos.reset(); + + SetInsertMode(bInsertMode); + Modify(); + + Invalidate(); + + // #i25161# call auto complete handler for ext text commit also + if (maAutocompleteHdl.IsSet()) + { + if ( (maSelection.Min() == maSelection.Max()) && (maSelection.Min() == maText.getLength()) ) + { + maAutocompleteHdl.Call(*this); + } + } + } + else if ( rCEvt.GetCommand() == CommandEventId::ExtTextInput ) + { + const CommandExtTextInputData* pData = rCEvt.GetExtTextInputData(); + + maText.remove( mpIMEInfos->nPos, mpIMEInfos->nLen ); + maText.insert( mpIMEInfos->nPos, pData->GetText() ); + if ( mpIMEInfos->bWasCursorOverwrite ) + { + const sal_Int32 nOldIMETextLen = mpIMEInfos->nLen; + const sal_Int32 nNewIMETextLen = pData->GetText().getLength(); + if ( ( nOldIMETextLen > nNewIMETextLen ) && + ( nNewIMETextLen < mpIMEInfos->aOldTextAfterStartPos.getLength() ) ) + { + // restore old characters + const sal_Int32 nRestore = nOldIMETextLen - nNewIMETextLen; + maText.insert( mpIMEInfos->nPos + nNewIMETextLen, mpIMEInfos->aOldTextAfterStartPos.subView( nNewIMETextLen, nRestore ) ); + } + else if ( ( nOldIMETextLen < nNewIMETextLen ) && + ( nOldIMETextLen < mpIMEInfos->aOldTextAfterStartPos.getLength() ) ) + { + const sal_Int32 nOverwrite = ( nNewIMETextLen > mpIMEInfos->aOldTextAfterStartPos.getLength() + ? mpIMEInfos->aOldTextAfterStartPos.getLength() : nNewIMETextLen ) - nOldIMETextLen; + maText.remove( mpIMEInfos->nPos + nNewIMETextLen, nOverwrite ); + } + } + + if ( pData->GetTextAttr() ) + { + mpIMEInfos->CopyAttribs( pData->GetTextAttr(), pData->GetText().getLength() ); + mpIMEInfos->bCursor = pData->IsCursorVisible(); + } + else + { + mpIMEInfos->DestroyAttribs(); + } + + ImplAlignAndPaint(); + sal_Int32 nCursorPos = mpIMEInfos->nPos + pData->GetCursorPos(); + SetSelection( Selection( nCursorPos, nCursorPos ) ); + SetInsertMode( !pData->IsCursorOverwrite() ); + + if ( pData->IsCursorVisible() ) + GetCursor()->Show(); + else + GetCursor()->Hide(); + } + else if ( rCEvt.GetCommand() == CommandEventId::CursorPos ) + { + if ( mpIMEInfos ) + { + sal_Int32 nCursorPos = GetSelection().Max(); + SetCursorRect( nullptr, GetTextWidth( maText.toString(), nCursorPos, mpIMEInfos->nPos+mpIMEInfos->nLen-nCursorPos ) ); + } + else + { + SetCursorRect(); + } + } + else if ( rCEvt.GetCommand() == CommandEventId::SelectionChange ) + { + const CommandSelectionChangeData *pData = rCEvt.GetSelectionChangeData(); + Selection aSelection( pData->GetStart(), pData->GetEnd() ); + SetSelection(aSelection); + } + else if ( rCEvt.GetCommand() == CommandEventId::QueryCharPosition ) + { + if (mpIMEInfos && mpIMEInfos->nLen > 0) + { + OUString aText = ImplGetText(); + std::vector<sal_Int32> aDX(2*(aText.getLength()+1)); + + GetOutDev()->GetCaretPositions( aText, aDX.data(), 0, aText.getLength() ); + + tools::Long nTH = GetTextHeight(); + Point aPos( mnXOffset, ImplGetTextYPosition() ); + + std::vector<tools::Rectangle> aRects(mpIMEInfos->nLen); + for ( int nIndex = 0; nIndex < mpIMEInfos->nLen; ++nIndex ) + { + tools::Rectangle aRect( aPos, Size( 10, nTH ) ); + aRect.SetLeft( aDX[2*(nIndex+mpIMEInfos->nPos)] + mnXOffset + ImplGetExtraXOffset() ); + aRects[ nIndex ] = aRect; + } + SetCompositionCharRect(aRects.data(), mpIMEInfos->nLen); + } + } + else + Control::Command( rCEvt ); +} + +void Edit::StateChanged( StateChangedType nType ) +{ + if (nType == StateChangedType::InitShow) + { + if (!mpSubEdit) + { + mnXOffset = 0; // if GrabFocus before while size was still wrong + ImplAlign(); + if (!mpSubEdit) + ImplShowCursor(false); + Invalidate(); + } + } + else if (nType == StateChangedType::Enable) + { + if (!mpSubEdit) + { + // change text color only + ImplInvalidateOrRepaint(); + } + } + else if (nType == StateChangedType::Style || nType == StateChangedType::Mirroring) + { + WinBits nStyle = GetStyle(); + if (nType == StateChangedType::Style) + { + nStyle = ImplInitStyle(GetStyle()); + SetStyle(nStyle); + } + + sal_uInt16 nOldAlign = mnAlign; + mnAlign = EDIT_ALIGN_LEFT; + + // hack: right align until keyinput and cursor travelling works + // edits are always RTL disabled + // however the parent edits contain the correct setting + if (mbIsSubEdit && GetParent()->IsRTLEnabled()) + { + if (GetParent()->GetStyle() & WB_LEFT) + mnAlign = EDIT_ALIGN_RIGHT; + if (nType == StateChangedType::Mirroring) + GetOutDev()->SetLayoutMode(vcl::text::ComplexTextLayoutFlags::BiDiRtl | vcl::text::ComplexTextLayoutFlags::TextOriginLeft); + } + else if (mbIsSubEdit && !GetParent()->IsRTLEnabled()) + { + if (nType == StateChangedType::Mirroring) + GetOutDev()->SetLayoutMode(vcl::text::ComplexTextLayoutFlags::TextOriginLeft); + } + + if (nStyle & WB_RIGHT) + mnAlign = EDIT_ALIGN_RIGHT; + else if (nStyle & WB_CENTER) + mnAlign = EDIT_ALIGN_CENTER; + if (!maText.isEmpty() && (mnAlign != nOldAlign)) + { + ImplAlign(); + Invalidate(); + } + + } + else if ((nType == StateChangedType::Zoom) || (nType == StateChangedType::ControlFont)) + { + if (!mpSubEdit) + { + ApplySettings(*GetOutDev()); + ImplShowCursor(); + Invalidate(); + } + } + else if ((nType == StateChangedType::ControlForeground) || (nType == StateChangedType::ControlBackground)) + { + if (!mpSubEdit) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + } + + Control::StateChanged(nType); +} + +void Edit::DataChanged( const DataChangedEvent& rDCEvt ) +{ + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + if ( !mpSubEdit ) + { + ApplySettings(*GetOutDev()); + ImplShowCursor(); + Invalidate(); + } + } + + Control::DataChanged( rDCEvt ); +} + +void Edit::ImplShowDDCursor() +{ + if (!mpDDInfo->bVisCursor) + { + tools::Long nTextWidth = GetTextWidth( maText.toString(), 0, mpDDInfo->nDropPos ); + tools::Long nTextHeight = GetTextHeight(); + tools::Rectangle aCursorRect( Point( nTextWidth + mnXOffset, (GetOutDev()->GetOutputSize().Height()-nTextHeight)/2 ), Size( 2, nTextHeight ) ); + mpDDInfo->aCursor.SetWindow( this ); + mpDDInfo->aCursor.SetPos( aCursorRect.TopLeft() ); + mpDDInfo->aCursor.SetSize( aCursorRect.GetSize() ); + mpDDInfo->aCursor.Show(); + mpDDInfo->bVisCursor = true; + } +} + +void Edit::ImplHideDDCursor() +{ + if ( mpDDInfo && mpDDInfo->bVisCursor ) + { + mpDDInfo->aCursor.Hide(); + mpDDInfo->bVisCursor = false; + } +} + +TextFilter::TextFilter(const OUString &rForbiddenChars) + : sForbiddenChars(rForbiddenChars) +{ +} + +TextFilter::~TextFilter() +{ +} + +OUString TextFilter::filter(const OUString &rText) +{ + OUString sTemp(rText); + for (sal_Int32 i = 0; i < sForbiddenChars.getLength(); ++i) + { + sTemp = sTemp.replaceAll(OUStringChar(sForbiddenChars[i]), ""); + } + return sTemp; +} + +void Edit::filterText() +{ + Selection aSel = GetSelection(); + const OUString sOrig = GetText(); + const OUString sNew = mpFilterText->filter(GetText()); + if (sOrig != sNew) + { + sal_Int32 nDiff = sOrig.getLength() - sNew.getLength(); + if (nDiff) + { + aSel.setMin(aSel.getMin() - nDiff); + aSel.setMax(aSel.getMin()); + } + SetText(sNew); + SetSelection(aSel); + } +} + +void Edit::Modify() +{ + if (mpFilterText) + filterText(); + + if ( mbIsSubEdit ) + { + static_cast<Edit*>(GetParent())->Modify(); + } + else + { + if ( ImplCallEventListenersAndHandler( VclEventId::EditModify, [this] () { maModifyHdl.Call(*this); } ) ) + // have been destroyed while calling into the handlers + return; + + // #i13677# notify edit listeners about caret position change + CallEventListeners( VclEventId::EditCaretChanged ); + // FIXME: this is currently only on macOS + // check for other platforms that need similar handling + if( ImplGetSVData()->maNWFData.mbNoFocusRects && + IsNativeWidgetEnabled() && + IsNativeControlSupported( ControlType::Editbox, ControlPart::Entire ) ) + { + ImplInvalidateOutermostBorder( this ); + } + } +} + +void Edit::SetEchoChar( sal_Unicode c ) +{ + mcEchoChar = c; + if ( mpSubEdit ) + mpSubEdit->SetEchoChar( c ); +} + +void Edit::SetReadOnly( bool bReadOnly ) +{ + if ( mbReadOnly != bReadOnly ) + { + mbReadOnly = bReadOnly; + if ( mpSubEdit ) + mpSubEdit->SetReadOnly( bReadOnly ); + + CompatStateChanged( StateChangedType::ReadOnly ); + } +} + +void Edit::SetInsertMode( bool bInsert ) +{ + if ( bInsert != mbInsertMode ) + { + mbInsertMode = bInsert; + if ( mpSubEdit ) + mpSubEdit->SetInsertMode( bInsert ); + else + ImplShowCursor(); + } +} + +bool Edit::IsInsertMode() const +{ + if ( mpSubEdit ) + return mpSubEdit->IsInsertMode(); + else + return mbInsertMode; +} + +void Edit::SetMaxTextLen(sal_Int32 nMaxLen) +{ + mnMaxTextLen = nMaxLen > 0 ? nMaxLen : EDIT_NOLIMIT; + + if ( mpSubEdit ) + mpSubEdit->SetMaxTextLen( mnMaxTextLen ); + else + { + if ( maText.getLength() > mnMaxTextLen ) + ImplDelete( Selection( mnMaxTextLen, maText.getLength() ), EDIT_DEL_RIGHT, EDIT_DELMODE_SIMPLE ); + } +} + +void Edit::SetSelection( const Selection& rSelection ) +{ + // If the selection was changed from outside, e.g. by MouseButtonDown, don't call Tracking() + // directly afterwards which would change the selection again + if ( IsTracking() ) + EndTracking(); + else if ( mpSubEdit && mpSubEdit->IsTracking() ) + mpSubEdit->EndTracking(); + + ImplSetSelection( rSelection ); +} + +void Edit::ImplSetSelection( const Selection& rSelection, bool bPaint ) +{ + if ( mpSubEdit ) + mpSubEdit->ImplSetSelection( rSelection ); + else + { + if ( rSelection != maSelection ) + { + Selection aOld( maSelection ); + Selection aNew( rSelection ); + + if ( aNew.Min() > maText.getLength() ) + aNew.Min() = maText.getLength(); + if ( aNew.Max() > maText.getLength() ) + aNew.Max() = maText.getLength(); + if ( aNew.Min() < 0 ) + aNew.Min() = 0; + if ( aNew.Max() < 0 ) + aNew.Max() = 0; + + if ( aNew != maSelection ) + { + ImplClearLayoutData(); + Selection aTemp = maSelection; + maSelection = aNew; + + if ( bPaint && ( aOld.Len() || aNew.Len() || IsPaintTransparent() ) ) + ImplInvalidateOrRepaint(); + ImplShowCursor(); + + bool bCaret = false, bSelection = false; + tools::Long nB=aNew.Max(), nA=aNew.Min(),oB=aTemp.Max(), oA=aTemp.Min(); + tools::Long nGap = nB-nA, oGap = oB-oA; + if (nB != oB) + bCaret = true; + if (nGap != 0 || oGap != 0) + bSelection = true; + + if (bSelection) + { + if ( mbIsSubEdit ) + static_cast<Edit*>(GetParent())->CallEventListeners( VclEventId::EditSelectionChanged ); + else + CallEventListeners( VclEventId::EditSelectionChanged ); + } + + if (bCaret) + { + if ( mbIsSubEdit ) + static_cast<Edit*>(GetParent())->CallEventListeners( VclEventId::EditCaretChanged ); + else + CallEventListeners( VclEventId::EditCaretChanged ); + } + + // #103511# notify combobox listeners of deselection + if( !maSelection && GetParent() && GetParent()->GetType() == WindowType::COMBOBOX ) + static_cast<Edit*>(GetParent())->CallEventListeners( VclEventId::ComboboxDeselect ); + } + } + } +} + +const Selection& Edit::GetSelection() const +{ + if ( mpSubEdit ) + return mpSubEdit->GetSelection(); + else + return maSelection; +} + +void Edit::ReplaceSelected( const OUString& rStr ) +{ + if ( mpSubEdit ) + mpSubEdit->ReplaceSelected( rStr ); + else + ImplInsertText( rStr ); +} + +void Edit::DeleteSelected() +{ + if ( mpSubEdit ) + mpSubEdit->DeleteSelected(); + else + { + if ( maSelection.Len() ) + ImplDelete( maSelection, EDIT_DEL_RIGHT, EDIT_DELMODE_SIMPLE ); + } +} + +OUString Edit::GetSelected() const +{ + if ( mpSubEdit ) + return mpSubEdit->GetSelected(); + else + { + Selection aSelection( maSelection ); + aSelection.Justify(); + return OUString( maText.getStr() + aSelection.Min(), aSelection.Len() ); + } +} + +void Edit::Cut() +{ + if ( !mbPassword ) + { + Copy(); + ReplaceSelected( OUString() ); + } +} + +void Edit::Copy() +{ + if ( !mbPassword ) + { + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aClipboard(GetClipboard()); + ImplCopy( aClipboard ); + } +} + +void Edit::Paste() +{ + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aClipboard(GetClipboard()); + ImplPaste( aClipboard ); +} + +void Edit::Undo() +{ + if ( mpSubEdit ) + mpSubEdit->Undo(); + else + { + const OUString aText( maText.toString() ); + ImplDelete( Selection( 0, aText.getLength() ), EDIT_DEL_RIGHT, EDIT_DELMODE_SIMPLE ); + ImplInsertText( maUndoText ); + ImplSetSelection( Selection( 0, maUndoText.getLength() ) ); + maUndoText = aText; + } +} + +void Edit::SetText( const OUString& rStr ) +{ + if ( mpSubEdit ) + mpSubEdit->SetText( rStr ); // not directly ImplSetText if SetText overridden + else + { + Selection aNewSel( 0, 0 ); // prevent scrolling + ImplSetText( rStr, &aNewSel ); + } +} + +void Edit::SetText( const OUString& rStr, const Selection& rSelection ) +{ + if ( mpSubEdit ) + mpSubEdit->SetText( rStr, rSelection ); + else + ImplSetText( rStr, &rSelection ); +} + +OUString Edit::GetText() const +{ + if ( mpSubEdit ) + return mpSubEdit->GetText(); + else + return maText.toString(); +} + +void Edit::SetCursorAtLast(){ + ImplSetCursorPos( GetText().getLength(), false ); +} + +void Edit::SetPlaceholderText( const OUString& rStr ) +{ + if ( mpSubEdit ) + mpSubEdit->SetPlaceholderText( rStr ); + else if ( maPlaceholderText != rStr ) + { + maPlaceholderText = rStr; + if ( GetText().isEmpty() ) + Invalidate(); + } +} + +void Edit::SetModifyFlag() +{ +} + +void Edit::SetSubEdit(Edit* pEdit) +{ + mpSubEdit.disposeAndClear(); + mpSubEdit.set(pEdit); + + if (mpSubEdit) + { + SetPointer(PointerStyle::Arrow); // Only SubEdit has the BEAM... + mpSubEdit->mbIsSubEdit = true; + + mpSubEdit->SetReadOnly(mbReadOnly); + mpSubEdit->maAutocompleteHdl = maAutocompleteHdl; + } +} + +Size Edit::CalcMinimumSizeForText(const OUString &rString) const +{ + ControlType eCtrlType = ImplGetNativeControlType(); + + Size aSize; + if (mnWidthInChars != -1) + { + //CalcSize calls CalcWindowSize, but we will call that also in this + //function, so undo the first one with CalcOutputSize + aSize = CalcOutputSize(CalcSize(mnWidthInChars)); + } + else + { + OUString aString; + if (mnMaxWidthChars != -1 && mnMaxWidthChars < rString.getLength()) + aString = rString.copy(0, mnMaxWidthChars); + else + aString = rString; + + aSize.setHeight( GetTextHeight() ); + aSize.setWidth( GetTextWidth(aString) ); + aSize.AdjustWidth(ImplGetExtraXOffset() * 2 ); + + // do not create edit fields in which one cannot enter anything + // a default minimum width should exist for at least 3 characters + + //CalcSize calls CalcWindowSize, but we will call that also in this + //function, so undo the first one with CalcOutputSize + Size aMinSize(CalcOutputSize(CalcSize(3))); + if (aSize.Width() < aMinSize.Width()) + aSize.setWidth( aMinSize.Width() ); + } + + aSize.AdjustHeight(ImplGetExtraYOffset() * 2 ); + + aSize = CalcWindowSize( aSize ); + + // ask NWF what if it has an opinion, too + ImplControlValue aControlValue; + tools::Rectangle aRect( Point( 0, 0 ), aSize ); + tools::Rectangle aContent, aBound; + if (GetNativeControlRegion(eCtrlType, ControlPart::Entire, aRect, ControlState::NONE, + aControlValue, aBound, aContent)) + { + if (aBound.GetHeight() > aSize.Height()) + aSize.setHeight( aBound.GetHeight() ); + } + return aSize; +} + +Size Edit::CalcMinimumSize() const +{ + return CalcMinimumSizeForText(GetText()); +} + +Size Edit::GetOptimalSize() const +{ + return CalcMinimumSize(); +} + +Size Edit::CalcSize(sal_Int32 nChars) const +{ + // width for N characters, independent from content. + // works only correct for fixed fonts, average otherwise + float fUnitWidth = std::max(approximate_char_width(), approximate_digit_width()); + Size aSz(fUnitWidth * nChars, GetTextHeight()); + aSz.AdjustWidth(ImplGetExtraXOffset() * 2 ); + aSz = CalcWindowSize( aSz ); + return aSz; +} + +sal_Int32 Edit::GetMaxVisChars() const +{ + const vcl::Window* pW = mpSubEdit ? mpSubEdit : this; + sal_Int32 nOutWidth = pW->GetOutputSizePixel().Width(); + float fUnitWidth = std::max(approximate_char_width(), approximate_digit_width()); + return nOutWidth / fUnitWidth; +} + +namespace vcl +{ + void SetGetSpecialCharsFunction( FncGetSpecialChars fn ) + { + pImplFncGetSpecialChars = fn; + } + + FncGetSpecialChars GetGetSpecialCharsFunction() + { + return pImplFncGetSpecialChars; + } +} + +VclPtr<PopupMenu> Edit::CreatePopupMenu() +{ + if (!mpUIBuilder) + mpUIBuilder.reset(new VclBuilder(nullptr, AllSettings::GetUIRootDir(), "vcl/ui/editmenu.ui", "")); + VclPtr<PopupMenu> pPopup = mpUIBuilder->get_menu("menu"); + const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); + if (rStyleSettings.GetHideDisabledMenuItems()) + pPopup->SetMenuFlags( MenuFlags::HideDisabledEntries ); + else + pPopup->SetMenuFlags ( MenuFlags::AlwaysShowDisabledEntries ); + if (rStyleSettings.GetContextMenuShortcuts()) + { + pPopup->SetAccelKey(pPopup->GetItemId("undo"), vcl::KeyCode( KeyFuncType::UNDO)); + pPopup->SetAccelKey(pPopup->GetItemId("cut"), vcl::KeyCode( KeyFuncType::CUT)); + pPopup->SetAccelKey(pPopup->GetItemId("copy"), vcl::KeyCode( KeyFuncType::COPY)); + pPopup->SetAccelKey(pPopup->GetItemId("paste"), vcl::KeyCode( KeyFuncType::PASTE)); + pPopup->SetAccelKey(pPopup->GetItemId("delete"), vcl::KeyCode( KeyFuncType::DELETE)); + pPopup->SetAccelKey(pPopup->GetItemId("selectall"), vcl::KeyCode( KEY_A, false, true, false, false)); + pPopup->SetAccelKey(pPopup->GetItemId("specialchar"), vcl::KeyCode( KEY_S, true, true, false, false)); + } + return pPopup; +} + +// css::datatransfer::dnd::XDragGestureListener +void Edit::dragGestureRecognized( const css::datatransfer::dnd::DragGestureEvent& rDGE ) +{ + SolarMutexGuard aVclGuard; + + if ( !(!IsTracking() && maSelection.Len() && + !mbPassword && (!mpDDInfo || !mpDDInfo->bStarterOfDD)) ) // no repeated D&D + return; + + Selection aSel( maSelection ); + aSel.Justify(); + + // only if mouse in the selection... + Point aMousePos( rDGE.DragOriginX, rDGE.DragOriginY ); + sal_Int32 nCharPos = ImplGetCharPos( aMousePos ); + if ( (nCharPos < aSel.Min()) || (nCharPos >= aSel.Max()) ) + return; + + if ( !mpDDInfo ) + mpDDInfo.reset(new DDInfo); + + mpDDInfo->bStarterOfDD = true; + mpDDInfo->aDndStartSel = aSel; + + if ( IsTracking() ) + EndTracking(); // before D&D disable tracking + + rtl::Reference<vcl::unohelper::TextDataObject> pDataObj = new vcl::unohelper::TextDataObject( GetSelected() ); + sal_Int8 nActions = datatransfer::dnd::DNDConstants::ACTION_COPY; + if ( !IsReadOnly() ) + nActions |= datatransfer::dnd::DNDConstants::ACTION_MOVE; + rDGE.DragSource->startDrag( rDGE, nActions, 0 /*cursor*/, 0 /*image*/, pDataObj, mxDnDListener ); + if ( GetCursor() ) + GetCursor()->Hide(); +} + +// css::datatransfer::dnd::XDragSourceListener +void Edit::dragDropEnd( const css::datatransfer::dnd::DragSourceDropEvent& rDSDE ) +{ + SolarMutexGuard aVclGuard; + + if (rDSDE.DropSuccess && (rDSDE.DropAction & datatransfer::dnd::DNDConstants::ACTION_MOVE) && mpDDInfo) + { + Selection aSel( mpDDInfo->aDndStartSel ); + if ( mpDDInfo->bDroppedInMe ) + { + if ( aSel.Max() > mpDDInfo->nDropPos ) + { + tools::Long nLen = aSel.Len(); + aSel.Min() += nLen; + aSel.Max() += nLen; + } + } + ImplDelete( aSel, EDIT_DEL_RIGHT, EDIT_DELMODE_SIMPLE ); + Modify(); + } + + ImplHideDDCursor(); + mpDDInfo.reset(); +} + +// css::datatransfer::dnd::XDropTargetListener +void Edit::drop( const css::datatransfer::dnd::DropTargetDropEvent& rDTDE ) +{ + SolarMutexGuard aVclGuard; + + bool bChanges = false; + if ( !mbReadOnly && mpDDInfo ) + { + ImplHideDDCursor(); + + Selection aSel( maSelection ); + aSel.Justify(); + + if ( aSel.Len() && !mpDDInfo->bStarterOfDD ) + ImplDelete( aSel, EDIT_DEL_RIGHT, EDIT_DELMODE_SIMPLE ); + + mpDDInfo->bDroppedInMe = true; + + aSel.Min() = mpDDInfo->nDropPos; + aSel.Max() = mpDDInfo->nDropPos; + ImplSetSelection( aSel ); + + uno::Reference< datatransfer::XTransferable > xDataObj = rDTDE.Transferable; + if ( xDataObj.is() ) + { + datatransfer::DataFlavor aFlavor; + SotExchange::GetFormatDataFlavor( SotClipboardFormatId::STRING, aFlavor ); + if ( xDataObj->isDataFlavorSupported( aFlavor ) ) + { + uno::Any aData = xDataObj->getTransferData( aFlavor ); + OUString aText; + aData >>= aText; + ImplInsertText( aText ); + bChanges = true; + Modify(); + } + } + + if ( !mpDDInfo->bStarterOfDD ) + { + mpDDInfo.reset(); + } + } + + rDTDE.Context->dropComplete( bChanges ); +} + +void Edit::dragEnter( const css::datatransfer::dnd::DropTargetDragEnterEvent& rDTDE ) +{ + if ( !mpDDInfo ) + { + mpDDInfo.reset(new DDInfo); + } + // search for string data type + const Sequence< css::datatransfer::DataFlavor >& rFlavors( rDTDE.SupportedDataFlavors ); + mpDDInfo->bIsStringSupported = std::any_of(rFlavors.begin(), rFlavors.end(), + [](const css::datatransfer::DataFlavor& rFlavor) { + sal_Int32 nIndex = 0; + const std::u16string_view aMimetype = o3tl::getToken(rFlavor.MimeType, 0, ';', nIndex ); + return aMimetype == u"text/plain"; + }); +} + +void Edit::dragExit( const css::datatransfer::dnd::DropTargetEvent& ) +{ + SolarMutexGuard aVclGuard; + + ImplHideDDCursor(); +} + +void Edit::dragOver( const css::datatransfer::dnd::DropTargetDragEvent& rDTDE ) +{ + SolarMutexGuard aVclGuard; + + Point aMousePos( rDTDE.LocationX, rDTDE.LocationY ); + + sal_Int32 nPrevDropPos = mpDDInfo->nDropPos; + mpDDInfo->nDropPos = ImplGetCharPos( aMousePos ); + + /* + Size aOutSize = GetOutputSizePixel(); + if ( ( aMousePos.X() < 0 ) || ( aMousePos.X() > aOutSize.Width() ) ) + { + // Scroll? + // No, I will not receive events in this case... + } + */ + + Selection aSel( maSelection ); + aSel.Justify(); + + // Don't accept drop in selection or read-only field... + if ( IsReadOnly() || aSel.Contains( mpDDInfo->nDropPos ) || ! mpDDInfo->bIsStringSupported ) + { + ImplHideDDCursor(); + rDTDE.Context->rejectDrag(); + } + else + { + // draw the old cursor away... + if ( !mpDDInfo->bVisCursor || ( nPrevDropPos != mpDDInfo->nDropPos ) ) + { + ImplHideDDCursor(); + ImplShowDDCursor(); + } + rDTDE.Context->acceptDrag( rDTDE.DropAction ); + } +} + +OUString Edit::GetSurroundingText() const +{ + if (mpSubEdit) + return mpSubEdit->GetSurroundingText(); + return maText.toString(); +} + +Selection Edit::GetSurroundingTextSelection() const +{ + return GetSelection(); +} + +bool Edit::DeleteSurroundingText(const Selection& rSelection) +{ + SetSelection(rSelection); + DeleteSelected(); + // maybe we should update mpIMEInfos here + return true; +} + +FactoryFunction Edit::GetUITestFactory() const +{ + return EditUIObject::create; +} + + +void Edit::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Control::DumpAsPropertyTree(rJsonWriter); + + if (!maPlaceholderText.isEmpty()) + rJsonWriter.put("placeholder", maPlaceholderText); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/field.cxx b/vcl/source/control/field.cxx new file mode 100644 index 000000000..90aef01f1 --- /dev/null +++ b/vcl/source/control/field.cxx @@ -0,0 +1,1885 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <cmath> +#include <string_view> + +#include <sal/log.hxx> +#include <o3tl/string_view.hxx> +#include <osl/diagnose.h> + +#include <comphelper/string.hxx> +#include <tools/UnitConversion.hxx> + +#include <vcl/builder.hxx> +#include <vcl/fieldvalues.hxx> +#include <vcl/toolkit/field.hxx> +#include <vcl/event.hxx> +#include <vcl/svapp.hxx> +#include <vcl/settings.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <vcl/uitest/metricfielduiobject.hxx> + +#include <svdata.hxx> + +#include <i18nutil/unicode.hxx> + +#include <rtl/math.hxx> + +#include <unotools/localedatawrapper.hxx> +#include <boost/property_tree/ptree.hpp> +#include <tools/json_writer.hxx> + +using namespace ::com::sun::star; +using namespace ::comphelper; + +namespace +{ + +std::string FieldUnitToString(FieldUnit unit) +{ + switch(unit) + { + case FieldUnit::NONE: + return ""; + + case FieldUnit::MM: + return "mm"; + + case FieldUnit::CM: + return "cm"; + + case FieldUnit::M: + return "m"; + + case FieldUnit::KM: + return "km"; + + case FieldUnit::TWIP: + return "twip"; + + case FieldUnit::POINT: + return "point"; + + case FieldUnit::PICA: + return "pica"; + + case FieldUnit::INCH: + return "inch"; + + case FieldUnit::FOOT: + return "foot"; + + case FieldUnit::MILE: + return "mile"; + + case FieldUnit::CHAR: + return "char"; + + case FieldUnit::LINE: + return "line"; + + case FieldUnit::CUSTOM: + return "custom"; + + case FieldUnit::PERCENT: + return "percent"; + + case FieldUnit::MM_100TH: + return "mm100th"; + + case FieldUnit::PIXEL: + return "pixel"; + + case FieldUnit::DEGREE: + return "degree"; + + case FieldUnit::SECOND: + return "second"; + + case FieldUnit::MILLISECOND: + return "millisecond"; + } + + return ""; +} + +sal_Int64 ImplPower10( sal_uInt16 n ) +{ + sal_uInt16 i; + sal_Int64 nValue = 1; + + for ( i=0; i < n; i++ ) + nValue *= 10; + + return nValue; +} + +bool ImplNumericProcessKeyInput( const KeyEvent& rKEvt, + bool bStrictFormat, bool bThousandSep, + const LocaleDataWrapper& rLocaleDataWrapper ) +{ + if ( !bStrictFormat ) + return false; + else + { + sal_Unicode cChar = rKEvt.GetCharCode(); + sal_uInt16 nGroup = rKEvt.GetKeyCode().GetGroup(); + + return !((nGroup == KEYGROUP_FKEYS) || + (nGroup == KEYGROUP_CURSOR) || + (nGroup == KEYGROUP_MISC) || + ((cChar >= '0') && (cChar <= '9')) || + rLocaleDataWrapper.getNumDecimalSep() == OUStringChar(cChar) || + (bThousandSep && rLocaleDataWrapper.getNumThousandSep() == OUStringChar(cChar)) || + rLocaleDataWrapper.getNumDecimalSepAlt() == OUStringChar(cChar) || + (cChar == '-')); + } +} + +bool ImplNumericGetValue( const OUString& rStr, sal_Int64& rValue, + sal_uInt16 nDecDigits, const LocaleDataWrapper& rLocaleDataWrapper, + bool bCurrency = false ) +{ + OUString aStr = rStr; + OUStringBuffer aStr1, aStr2, aStrNum, aStrDenom; + bool bNegative = false; + bool bFrac = false; + sal_Int32 nDecPos, nFracDivPos; + sal_Int64 nValue; + + // react on empty string + if ( rStr.isEmpty() ) + return false; + + // remove leading and trailing spaces + aStr = aStr.trim(); + + + // find position of decimal point + nDecPos = aStr.indexOf( rLocaleDataWrapper.getNumDecimalSep() ); + if (nDecPos < 0 && !rLocaleDataWrapper.getNumDecimalSepAlt().isEmpty()) + nDecPos = aStr.indexOf( rLocaleDataWrapper.getNumDecimalSepAlt() ); + // find position of fraction + nFracDivPos = aStr.indexOf( '/' ); + + // parse fractional strings + if (nFracDivPos > 0) + { + bFrac = true; + sal_Int32 nFracNumPos = aStr.lastIndexOf(' ', nFracDivPos); + + // If in "a b/c" format. + if(nFracNumPos != -1 ) + { + aStr1.append(aStr.subView(0, nFracNumPos)); + aStrNum.append(aStr.subView(nFracNumPos+1, nFracDivPos-nFracNumPos-1)); + aStrDenom.append(aStr.subView(nFracDivPos+1)); + } + // "a/b" format, or not a fraction at all + else + { + aStrNum.append(aStr.subView(0, nFracDivPos)); + aStrDenom.append(aStr.subView(nFracDivPos+1)); + } + + } + // parse decimal strings + else if ( nDecPos >= 0) + { + aStr1.append(aStr.subView(0, nDecPos)); + aStr2.append(aStr.subView(nDecPos+1)); + } + else + aStr1 = aStr; + + // negative? + if ( bCurrency ) + { + if ( aStr.startsWith("(") && aStr.endsWith(")") ) + bNegative = true; + if ( !bNegative ) + { + for (sal_Int32 i=0; i < aStr.getLength(); i++ ) + { + if ( (aStr[i] >= '0') && (aStr[i] <= '9') ) + break; + else if ( aStr[i] == '-' ) + { + bNegative = true; + break; + } + } + } + if (!bNegative && !aStr.isEmpty()) + { + sal_uInt16 nFormat = rLocaleDataWrapper.getCurrNegativeFormat(); + if ( (nFormat == 3) || (nFormat == 6) || // $1- || 1-$ + (nFormat == 7) || (nFormat == 10) ) // 1$- || 1 $- + { + for (sal_Int32 i = aStr.getLength()-1; i > 0; --i ) + { + if ( (aStr[i] >= '0') && (aStr[i] <= '9') ) + break; + else if ( aStr[i] == '-' ) + { + bNegative = true; + break; + } + } + } + } + } + else + { + if ( !aStr1.isEmpty() && aStr1[0] == '-') + bNegative = true; + if ( !aStrNum.isEmpty() && aStrNum[0] == '-') // For non-mixed fractions + bNegative = true; + } + + // remove all unwanted characters + // For whole number + for (sal_Int32 i=0; i < aStr1.getLength(); ) + { + if ( (aStr1[i] >= '0') && (aStr1[i] <= '9') ) + i++; + else + aStr1.remove( i, 1 ); + } + // For decimal + if (!bFrac) { + for (sal_Int32 i=0; i < aStr2.getLength(); ) + { + if ((aStr2[i] >= '0') && (aStr2[i] <= '9')) + ++i; + else + aStr2.remove(i, 1); + } + } + else { + // for numerator + for (sal_Int32 i=0; i < aStrNum.getLength(); ) + { + if ((aStrNum[i] >= '0') && (aStrNum[i] <= '9')) + ++i; + else + aStrNum.remove(i, 1); + } + // for denominator + for (sal_Int32 i=0; i < aStrDenom.getLength(); ) + { + if ((aStrDenom[i] >= '0') && (aStrDenom[i] <= '9')) + ++i; + else + aStrDenom.remove(i, 1); + } + } + + + if ( !bFrac && aStr1.isEmpty() && aStr2.isEmpty() ) + return false; + else if ( bFrac && aStr1.isEmpty() && (aStrNum.isEmpty() || aStrDenom.isEmpty()) ) + return false; + + if ( aStr1.isEmpty() ) + aStr1 = "0"; + if ( bNegative ) + aStr1.insert(0, "-"); + + // Convert fractional strings + if (bFrac) { + // Convert to fraction + sal_Int64 nWholeNum = o3tl::toInt64(aStr1); + aStr1.setLength(0); + sal_Int64 nNum = o3tl::toInt64(aStrNum); + sal_Int64 nDenom = o3tl::toInt64(aStrDenom); + if (nDenom == 0) return false; // Division by zero + double nFrac2Dec = nWholeNum + static_cast<double>(nNum)/nDenom; // Convert to double for floating point precision + OUStringBuffer aStrFrac; + aStrFrac.append(nFrac2Dec); + // Reconvert division result to string and parse + nDecPos = aStrFrac.indexOf('.'); + if ( nDecPos >= 0) + { + aStr1.append(aStrFrac.getStr(), nDecPos); + aStr2.append(aStrFrac.getStr()+nDecPos+1); + } + else + aStr1 = aStrFrac; + } + + // prune and round fraction + bool bRound = false; + if (aStr2.getLength() > nDecDigits) + { + if (aStr2[nDecDigits] >= '5') + bRound = true; + string::truncateToLength(aStr2, nDecDigits); + } + if (aStr2.getLength() < nDecDigits) + string::padToLength(aStr2, nDecDigits, '0'); + + aStr = aStr1 + aStr2; + + // check range + nValue = aStr.toInt64(); + if( nValue == 0 ) + { + // check if string is equivalent to zero + sal_Int32 nIndex = bNegative ? 1 : 0; + while (nIndex < aStr.getLength() && aStr[nIndex] == '0') + ++nIndex; + if( nIndex < aStr.getLength() ) + { + rValue = bNegative ? SAL_MIN_INT64 : SAL_MAX_INT64; + return true; + } + } + if (bRound) + { + if ( !bNegative ) + nValue++; + else + nValue--; + } + + rValue = nValue; + + return true; +} + +void ImplUpdateSeparatorString( OUString& io_rText, + const OUString& rOldDecSep, std::u16string_view rNewDecSep, + const OUString& rOldThSep, std::u16string_view rNewThSep ) +{ + OUStringBuffer aBuf( io_rText.getLength() ); + sal_Int32 nIndexDec = 0, nIndexTh = 0, nIndex = 0; + + const sal_Unicode* pBuffer = io_rText.getStr(); + while( nIndex != -1 ) + { + nIndexDec = io_rText.indexOf( rOldDecSep, nIndex ); + nIndexTh = io_rText.indexOf( rOldThSep, nIndex ); + if( (nIndexTh != -1 && nIndexDec != -1 && nIndexTh < nIndexDec ) + || (nIndexTh != -1 && nIndexDec == -1) + ) + { + aBuf.append( pBuffer + nIndex, nIndexTh - nIndex ); + aBuf.append( rNewThSep ); + nIndex = nIndexTh + rOldThSep.getLength(); + } + else if( nIndexDec != -1 ) + { + aBuf.append( pBuffer + nIndex, nIndexDec - nIndex ); + aBuf.append( rNewDecSep ); + nIndex = nIndexDec + rOldDecSep.getLength(); + } + else + { + aBuf.append( pBuffer + nIndex ); + nIndex = -1; + } + } + + io_rText = aBuf.makeStringAndClear(); +} + +void ImplUpdateSeparators( const OUString& rOldDecSep, std::u16string_view rNewDecSep, + const OUString& rOldThSep, std::u16string_view rNewThSep, + Edit* pEdit ) +{ + bool bChangeDec = (rOldDecSep != rNewDecSep); + bool bChangeTh = (rOldThSep != rNewThSep ); + + if( !(bChangeDec || bChangeTh) ) + return; + + bool bUpdateMode = pEdit->IsUpdateMode(); + pEdit->SetUpdateMode( false ); + OUString aText = pEdit->GetText(); + ImplUpdateSeparatorString( aText, rOldDecSep, rNewDecSep, rOldThSep, rNewThSep ); + pEdit->SetText( aText ); + + ComboBox* pCombo = dynamic_cast<ComboBox*>(pEdit); + if( pCombo ) + { + // update box entries + sal_Int32 nEntryCount = pCombo->GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; i++ ) + { + aText = pCombo->GetEntry( i ); + void* pEntryData = pCombo->GetEntryData( i ); + ImplUpdateSeparatorString( aText, rOldDecSep, rNewDecSep, rOldThSep, rNewThSep ); + pCombo->RemoveEntryAt(i); + pCombo->InsertEntry( aText, i ); + pCombo->SetEntryData( i, pEntryData ); + } + } + if( bUpdateMode ) + pEdit->SetUpdateMode( bUpdateMode ); +} + +} // namespace + +FormatterBase::FormatterBase(Edit* pField) +{ + mpField = pField; + mpLocaleDataWrapper = nullptr; + mbReformat = false; + mbStrictFormat = false; + mbEmptyFieldValue = false; + mbEmptyFieldValueEnabled = false; +} + +FormatterBase::~FormatterBase() +{ +} + +LocaleDataWrapper& FormatterBase::ImplGetLocaleDataWrapper() const +{ + if ( !mpLocaleDataWrapper ) + { + mpLocaleDataWrapper.reset( new LocaleDataWrapper( GetLanguageTag() ) ); + } + return *mpLocaleDataWrapper; +} + +/** reset the LocaleDataWrapper when the language tag changes */ +void FormatterBase::ImplResetLocaleDataWrapper() const +{ + // just get rid of, the next time it is requested, it will get loaded with the right + // language tag + mpLocaleDataWrapper.reset(); +} + +const LocaleDataWrapper& FormatterBase::GetLocaleDataWrapper() const +{ + return ImplGetLocaleDataWrapper(); +} + +void FormatterBase::Reformat() +{ +} + +void FormatterBase::ReformatAll() +{ + Reformat(); +}; + +void FormatterBase::SetStrictFormat( bool bStrict ) +{ + if ( bStrict != mbStrictFormat ) + { + mbStrictFormat = bStrict; + if ( mbStrictFormat ) + ReformatAll(); + } +} + +const lang::Locale& FormatterBase::GetLocale() const +{ + if ( mpField ) + return mpField->GetSettings().GetLanguageTag().getLocale(); + else + return Application::GetSettings().GetLanguageTag().getLocale(); +} + +const LanguageTag& FormatterBase::GetLanguageTag() const +{ + if ( mpField ) + return mpField->GetSettings().GetLanguageTag(); + else + return Application::GetSettings().GetLanguageTag(); +} + +void FormatterBase::ImplSetText( const OUString& rText, Selection const * pNewSelection ) +{ + if ( mpField ) + { + if (pNewSelection) + mpField->SetText(rText, *pNewSelection); + else + { + Selection aSel = mpField->GetSelection(); + aSel.Min() = aSel.Max(); + mpField->SetText(rText, aSel); + } + MarkToBeReformatted( false ); + } +} + +void FormatterBase::SetEmptyFieldValue() +{ + if ( mpField ) + mpField->SetText( OUString() ); + mbEmptyFieldValue = true; +} + +bool FormatterBase::IsEmptyFieldValue() const +{ + return (!mpField || mpField->GetText().isEmpty()); +} + +void NumericFormatter::FormatValue(Selection const * pNewSelection) +{ + mbFormatting = true; + ImplSetText(CreateFieldText(mnLastValue), pNewSelection); + mbFormatting = false; +} + +void NumericFormatter::ImplNumericReformat() +{ + mnLastValue = GetValue(); + FormatValue(); +} + +NumericFormatter::NumericFormatter(Edit* pEdit) + : FormatterBase(pEdit) + , mnLastValue(0) + , mnMin(0) + // a "large" value substantially smaller than SAL_MAX_INT64, to avoid + // overflow in computations using this "dummy" value + , mnMax(SAL_MAX_INT32) + , mbFormatting(false) + , mnSpinSize(1) + // for fields + , mnFirst(mnMin) + , mnLast(mnMax) + , mnDecimalDigits(0) + , mbThousandSep(true) +{ + ReformatAll(); +} + +NumericFormatter::~NumericFormatter() +{ +} + +void NumericFormatter::SetMin( sal_Int64 nNewMin ) +{ + mnMin = nNewMin; + if ( !IsEmptyFieldValue() ) + ReformatAll(); +} + +void NumericFormatter::SetMax( sal_Int64 nNewMax ) +{ + mnMax = nNewMax; + if ( !IsEmptyFieldValue() ) + ReformatAll(); +} + +void NumericFormatter::SetUseThousandSep( bool bValue ) +{ + mbThousandSep = bValue; + ReformatAll(); +} + +void NumericFormatter::SetDecimalDigits( sal_uInt16 nDigits ) +{ + mnDecimalDigits = nDigits; + ReformatAll(); +} + +void NumericFormatter::SetValue( sal_Int64 nNewValue ) +{ + SetUserValue( nNewValue ); + SetEmptyFieldValueData( false ); +} + +OUString NumericFormatter::CreateFieldText( sal_Int64 nValue ) const +{ + return ImplGetLocaleDataWrapper().getNum( nValue, GetDecimalDigits(), IsUseThousandSep(), /*ShowTrailingZeros*/true ); +} + +void NumericFormatter::ImplSetUserValue( sal_Int64 nNewValue, Selection const * pNewSelection ) +{ + nNewValue = ClipAgainstMinMax(nNewValue); + mnLastValue = nNewValue; + + if ( GetField() ) + FormatValue(pNewSelection); +} + +void NumericFormatter::SetUserValue( sal_Int64 nNewValue ) +{ + ImplSetUserValue( nNewValue ); +} + +sal_Int64 NumericFormatter::GetValueFromString(const OUString& rStr) const +{ + sal_Int64 nTempValue; + + if (ImplNumericGetValue(rStr, nTempValue, + GetDecimalDigits(), ImplGetLocaleDataWrapper())) + { + return ClipAgainstMinMax(nTempValue); + } + else + return mnLastValue; +} + +OUString NumericFormatter::GetValueString() const +{ + return Application::GetSettings().GetNeutralLocaleDataWrapper(). + getNum(GetValue(), GetDecimalDigits(), false, false); +} + +// currently used by online +void NumericFormatter::SetValueFromString(const OUString& rStr) +{ + sal_Int64 nValue; + + if (ImplNumericGetValue(rStr, nValue, GetDecimalDigits(), + Application::GetSettings().GetNeutralLocaleDataWrapper())) + { + ImplNewFieldValue(nValue); + } + else + { + SAL_WARN("vcl", "fail to convert the value: " << rStr ); + } +} + +sal_Int64 NumericFormatter::GetValue() const +{ + if (mbFormatting) //don't parse the entry if we're currently formatting what to put in it + return mnLastValue; + + return GetField() ? GetValueFromString(GetField()->GetText()) : 0; +} + +sal_Int64 NumericFormatter::Normalize( sal_Int64 nValue ) const +{ + return (nValue * ImplPower10( GetDecimalDigits() ) ); +} + +sal_Int64 NumericFormatter::Denormalize( sal_Int64 nValue ) const +{ + sal_Int64 nFactor = ImplPower10( GetDecimalDigits() ); + + if ((nValue < ( SAL_MIN_INT64 + nFactor )) || + (nValue > ( SAL_MAX_INT64 - nFactor ))) + { + return ( nValue / nFactor ); + } + + if( nValue < 0 ) + { + sal_Int64 nHalf = nFactor / 2; + return ((nValue - nHalf) / nFactor ); + } + else + { + sal_Int64 nHalf = nFactor / 2; + return ((nValue + nHalf) / nFactor ); + } +} + +void NumericFormatter::Reformat() +{ + if ( !GetField() ) + return; + + if ( GetField()->GetText().isEmpty() && ImplGetEmptyFieldValue() ) + return; + + ImplNumericReformat(); +} + +void NumericFormatter::FieldUp() +{ + sal_Int64 nValue = GetValue(); + sal_Int64 nRemainder = nValue % mnSpinSize; + if (nValue >= 0) + nValue = (nRemainder == 0) ? nValue + mnSpinSize : nValue + mnSpinSize - nRemainder; + else + nValue = (nRemainder == 0) ? nValue + mnSpinSize : nValue - nRemainder; + + nValue = ClipAgainstMinMax(nValue); + + ImplNewFieldValue( nValue ); +} + +void NumericFormatter::FieldDown() +{ + sal_Int64 nValue = GetValue(); + sal_Int64 nRemainder = nValue % mnSpinSize; + if (nValue >= 0) + nValue = (nRemainder == 0) ? nValue - mnSpinSize : nValue - nRemainder; + else + nValue = (nRemainder == 0) ? nValue - mnSpinSize : nValue - mnSpinSize - nRemainder; + + nValue = ClipAgainstMinMax(nValue); + + ImplNewFieldValue( nValue ); +} + +void NumericFormatter::FieldFirst() +{ + ImplNewFieldValue( mnFirst ); +} + +void NumericFormatter::FieldLast() +{ + ImplNewFieldValue( mnLast ); +} + +void NumericFormatter::ImplNewFieldValue( sal_Int64 nNewValue ) +{ + if ( !GetField() ) + return; + + // !!! We should check why we do not validate in ImplSetUserValue() if the value was + // changed. This should be done there as well since otherwise the call to Modify would not + // be allowed. Anyway, the paths from ImplNewFieldValue, ImplSetUserValue, and ImplSetText + // should be checked and clearly traced (with comment) in order to find out what happens. + + Selection aSelection = GetField()->GetSelection(); + aSelection.Justify(); + OUString aText = GetField()->GetText(); + // leave it as is if selected until end + if ( static_cast<sal_Int32>(aSelection.Max()) == aText.getLength() ) + { + if ( !aSelection.Len() ) + aSelection.Min() = SELECTION_MAX; + aSelection.Max() = SELECTION_MAX; + } + + sal_Int64 nOldLastValue = mnLastValue; + ImplSetUserValue( nNewValue, &aSelection ); + mnLastValue = nOldLastValue; + + // Modify during Edit is only set during KeyInput + if ( GetField()->GetText() != aText ) + { + GetField()->SetModifyFlag(); + GetField()->Modify(); + } +} + +sal_Int64 NumericFormatter::ClipAgainstMinMax(sal_Int64 nValue) const +{ + if (nValue > mnMax) + nValue = mnMax; + else if (nValue < mnMin) + nValue = mnMin; + return nValue; +} + +namespace +{ + Size calcMinimumSize(const Edit &rSpinField, const NumericFormatter &rFormatter) + { + OUStringBuffer aBuf; + sal_Int32 nTextLen; + + nTextLen = OUString(OUString::number(rFormatter.GetMin())).getLength(); + string::padToLength(aBuf, nTextLen, '9'); + Size aMinTextSize = rSpinField.CalcMinimumSizeForText( + rFormatter.CreateFieldText(OUString::unacquired(aBuf).toInt64())); + aBuf.setLength(0); + + nTextLen = OUString(OUString::number(rFormatter.GetMax())).getLength(); + string::padToLength(aBuf, nTextLen, '9'); + Size aMaxTextSize = rSpinField.CalcMinimumSizeForText( + rFormatter.CreateFieldText(OUString::unacquired(aBuf).toInt64())); + aBuf.setLength(0); + + Size aRet(std::max(aMinTextSize.Width(), aMaxTextSize.Width()), + std::max(aMinTextSize.Height(), aMaxTextSize.Height())); + + OUStringBuffer sBuf("999999999"); + sal_uInt16 nDigits = rFormatter.GetDecimalDigits(); + if (nDigits) + { + sBuf.append('.'); + string::padToLength(aBuf, aBuf.getLength() + nDigits, '9'); + } + aMaxTextSize = rSpinField.CalcMinimumSizeForText(sBuf.makeStringAndClear()); + aRet.setWidth( std::min(aRet.Width(), aMaxTextSize.Width()) ); + + return aRet; + } +} + +NumericBox::NumericBox(vcl::Window* pParent, WinBits nWinStyle) + : ComboBox(pParent, nWinStyle) + , NumericFormatter(this) +{ + Reformat(); + if ( !(nWinStyle & WB_HIDE ) ) + Show(); +} + +void NumericBox::dispose() +{ + ClearField(); + ComboBox::dispose(); +} + +Size NumericBox::CalcMinimumSize() const +{ + Size aRet(calcMinimumSize(*this, *this)); + + if (IsDropDownBox()) + { + Size aComboSugg(ComboBox::CalcMinimumSize()); + aRet.setWidth( std::max(aRet.Width(), aComboSugg.Width()) ); + aRet.setHeight( std::max(aRet.Height(), aComboSugg.Height()) ); + } + + return aRet; +} + +bool NumericBox::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplNumericProcessKeyInput( *rNEvt.GetKeyEvent(), IsStrictFormat(), IsUseThousandSep(), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return ComboBox::PreNotify( rNEvt ); +} + +bool NumericBox::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return ComboBox::EventNotify( rNEvt ); +} + +void NumericBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + ComboBox::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + OUString sOldDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sOldThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplResetLocaleDataWrapper(); + OUString sNewDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sNewThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplUpdateSeparators( sOldDecSep, sNewDecSep, sOldThSep, sNewThSep, this ); + ReformatAll(); + } +} + +void NumericBox::Modify() +{ + MarkToBeReformatted( true ); + ComboBox::Modify(); +} + +void NumericBox::ImplNumericReformat( const OUString& rStr, sal_Int64& rValue, + OUString& rOutStr ) +{ + if (ImplNumericGetValue(rStr, rValue, GetDecimalDigits(), ImplGetLocaleDataWrapper())) + { + sal_Int64 nTempVal = ClipAgainstMinMax(rValue); + rOutStr = CreateFieldText( nTempVal ); + } +} + +void NumericBox::ReformatAll() +{ + sal_Int64 nValue; + OUString aStr; + SetUpdateMode( false ); + sal_Int32 nEntryCount = GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; i++ ) + { + ImplNumericReformat( GetEntry( i ), nValue, aStr ); + RemoveEntryAt(i); + InsertEntry( aStr, i ); + } + NumericFormatter::Reformat(); + SetUpdateMode( true ); +} + +static bool ImplMetricProcessKeyInput( const KeyEvent& rKEvt, + bool bUseThousandSep, const LocaleDataWrapper& rWrapper ) +{ + // no meaningful strict format; therefore allow all characters + return ImplNumericProcessKeyInput( rKEvt, false, bUseThousandSep, rWrapper ); +} + +static OUString ImplMetricGetUnitText(const OUString& rStr) +{ + // fetch unit text + OUStringBuffer aStr; + for (sal_Int32 i = rStr.getLength()-1; i >= 0; --i) + { + sal_Unicode c = rStr[i]; + if ( (c == '\'') || (c == '\"') || (c == '%') || (c == 0x2032) || (c == 0x2033) || unicode::isAlpha(c) || unicode::isControl(c) ) + aStr.insert(0, c); + else + { + if (!aStr.isEmpty()) + break; + } + } + return aStr.makeStringAndClear(); +} + +// #104355# support localized measurements + +static OUString ImplMetricToString( FieldUnit rUnit ) +{ + // return unit's default string (ie, the first one ) + for (auto const& elem : ImplGetFieldUnits()) + { + if (elem.second == rUnit) + return elem.first; + } + + return OUString(); +} + +namespace +{ + FieldUnit StringToMetric(const OUString &rMetricString) + { + // return FieldUnit + OUString aStr = rMetricString.toAsciiLowerCase().replaceAll(" ", ""); + for (auto const& elem : ImplGetCleanedFieldUnits()) + { + if ( elem.first == aStr ) + return elem.second; + } + + return FieldUnit::NONE; + } +} + +static FieldUnit ImplMetricGetUnit(const OUString& rStr) +{ + OUString aStr = ImplMetricGetUnitText(rStr); + return StringToMetric(aStr); +} + +static FieldUnit ImplMap2FieldUnit( MapUnit meUnit, tools::Long& nDecDigits ) +{ + switch( meUnit ) + { + case MapUnit::Map100thMM : + nDecDigits -= 2; + return FieldUnit::MM; + case MapUnit::Map10thMM : + nDecDigits -= 1; + return FieldUnit::MM; + case MapUnit::MapMM : + return FieldUnit::MM; + case MapUnit::MapCM : + return FieldUnit::CM; + case MapUnit::Map1000thInch : + nDecDigits -= 3; + return FieldUnit::INCH; + case MapUnit::Map100thInch : + nDecDigits -= 2; + return FieldUnit::INCH; + case MapUnit::Map10thInch : + nDecDigits -= 1; + return FieldUnit::INCH; + case MapUnit::MapInch : + return FieldUnit::INCH; + case MapUnit::MapPoint : + return FieldUnit::POINT; + case MapUnit::MapTwip : + return FieldUnit::TWIP; + default: + OSL_FAIL( "default eInUnit" ); + break; + } + return FieldUnit::NONE; +} + +static double nonValueDoubleToValueDouble( double nValue ) +{ + return std::isfinite( nValue ) ? nValue : 0.0; +} + +namespace vcl +{ + sal_Int64 ConvertValue(sal_Int64 nValue, sal_Int64 mnBaseValue, sal_uInt16 nDecDigits, + FieldUnit eInUnit, FieldUnit eOutUnit) + { + double nDouble = nonValueDoubleToValueDouble(vcl::ConvertDoubleValue( + static_cast<double>(nValue), mnBaseValue, nDecDigits, eInUnit, eOutUnit)); + sal_Int64 nLong ; + + // caution: precision loss in double cast + if ( nDouble <= double(SAL_MIN_INT64) ) + nLong = SAL_MIN_INT64; + else if ( nDouble >= double(SAL_MAX_INT64) ) + nLong = SAL_MAX_INT64; + else + nLong = static_cast<sal_Int64>( std::round(nDouble) ); + + return nLong; + } +} + +namespace { + +bool checkConversionUnits(MapUnit eInUnit, FieldUnit eOutUnit) +{ + return eOutUnit != FieldUnit::PERCENT + && eOutUnit != FieldUnit::CUSTOM + && eOutUnit != FieldUnit::NONE + && eInUnit != MapUnit::MapPixel + && eInUnit != MapUnit::MapSysFont + && eInUnit != MapUnit::MapAppFont + && eInUnit != MapUnit::MapRelative; +} + +double convertValue( double nValue, tools::Long nDigits, FieldUnit eInUnit, FieldUnit eOutUnit ) +{ + if ( nDigits < 0 ) + { + while ( nDigits ) + { + nValue += 5; + nValue /= 10; + nDigits++; + } + } + else + { + nValue *= ImplPower10(nDigits); + } + + if ( eInUnit != eOutUnit ) + { + const o3tl::Length eFrom = FieldToO3tlLength(eInUnit), eTo = FieldToO3tlLength(eOutUnit); + if (eFrom != o3tl::Length::invalid && eTo != o3tl::Length::invalid) + nValue = o3tl::convert(nValue, eFrom, eTo); + } + + return nValue; +} + +} + +namespace vcl +{ + sal_Int64 ConvertValue( sal_Int64 nValue, sal_uInt16 nDigits, + MapUnit eInUnit, FieldUnit eOutUnit ) + { + if ( !checkConversionUnits(eInUnit, eOutUnit) ) + { + OSL_FAIL( "invalid parameters" ); + return nValue; + } + + tools::Long nDecDigits = nDigits; + FieldUnit eFieldUnit = ImplMap2FieldUnit( eInUnit, nDecDigits ); + + // Avoid sal_Int64 <-> double conversion issues if possible: + if (eFieldUnit == eOutUnit && nDigits == 0) + { + return nValue; + } + + return static_cast<sal_Int64>( + nonValueDoubleToValueDouble( + convertValue( nValue, nDecDigits, eFieldUnit, eOutUnit ) ) ); + } + + double ConvertDoubleValue(double nValue, sal_Int64 mnBaseValue, sal_uInt16 nDecDigits, + FieldUnit eInUnit, FieldUnit eOutUnit) + { + if ( eInUnit != eOutUnit ) + { + if (eInUnit == FieldUnit::PERCENT && mnBaseValue > 0 && nValue > 0) + { + sal_Int64 nDiv = 100 * ImplPower10(nDecDigits); + + if (mnBaseValue != 1) + nValue *= mnBaseValue; + + nValue += nDiv / 2; + nValue /= nDiv; + } + else + { + const o3tl::Length eFrom = FieldToO3tlLength(eInUnit, o3tl::Length::invalid); + const o3tl::Length eTo = FieldToO3tlLength(eOutUnit, o3tl::Length::invalid); + if (eFrom != o3tl::Length::invalid && eTo != o3tl::Length::invalid) + nValue = o3tl::convert(nValue, eFrom, eTo); + } + } + + return nValue; + } + + double ConvertDoubleValue(double nValue, sal_uInt16 nDigits, + MapUnit eInUnit, FieldUnit eOutUnit) + { + if ( !checkConversionUnits(eInUnit, eOutUnit) ) + { + OSL_FAIL( "invalid parameters" ); + return nValue; + } + + tools::Long nDecDigits = nDigits; + FieldUnit eFieldUnit = ImplMap2FieldUnit( eInUnit, nDecDigits ); + + return convertValue(nValue, nDecDigits, eFieldUnit, eOutUnit); + } + + double ConvertDoubleValue(double nValue, sal_uInt16 nDigits, + FieldUnit eInUnit, MapUnit eOutUnit) + { + if ( eInUnit == FieldUnit::PERCENT || + eInUnit == FieldUnit::CUSTOM || + eInUnit == FieldUnit::NONE || + eInUnit == FieldUnit::DEGREE || + eInUnit == FieldUnit::SECOND || + eInUnit == FieldUnit::MILLISECOND || + eInUnit == FieldUnit::PIXEL || + eOutUnit == MapUnit::MapPixel || + eOutUnit == MapUnit::MapSysFont || + eOutUnit == MapUnit::MapAppFont || + eOutUnit == MapUnit::MapRelative ) + { + OSL_FAIL( "invalid parameters" ); + return nValue; + } + + tools::Long nDecDigits = nDigits; + FieldUnit eFieldUnit = ImplMap2FieldUnit( eOutUnit, nDecDigits ); + + if ( nDecDigits < 0 ) + { + nValue *= ImplPower10(-nDecDigits); + } + else + { + nValue /= ImplPower10(nDecDigits); + } + + if ( eFieldUnit != eInUnit ) + { + const o3tl::Length eFrom = FieldToO3tlLength(eInUnit, o3tl::Length::invalid); + const o3tl::Length eTo = FieldToO3tlLength(eFieldUnit, o3tl::Length::invalid); + if (eFrom != o3tl::Length::invalid && eTo != o3tl::Length::invalid) + nValue = o3tl::convert(nValue, eFrom, eTo); + } + return nValue; + } +} + +namespace vcl +{ + bool TextToValue(const OUString& rStr, double& rValue, sal_Int64 nBaseValue, + sal_uInt16 nDecDigits, const LocaleDataWrapper& rLocaleDataWrapper, FieldUnit eUnit) + { + // Get value + sal_Int64 nValue; + if ( !ImplNumericGetValue( rStr, nValue, nDecDigits, rLocaleDataWrapper ) ) + return false; + + // Determine unit + FieldUnit eEntryUnit = ImplMetricGetUnit( rStr ); + + // Recalculate unit + // caution: conversion to double loses precision + rValue = vcl::ConvertDoubleValue(static_cast<double>(nValue), nBaseValue, nDecDigits, eEntryUnit, eUnit); + + return true; + } +} + +void MetricFormatter::ImplMetricReformat( const OUString& rStr, double& rValue, OUString& rOutStr ) +{ + if (!vcl::TextToValue(rStr, rValue, 0, GetDecimalDigits(), ImplGetLocaleDataWrapper(), meUnit)) + return; + + double nTempVal = rValue; + // caution: precision loss in double cast + if ( nTempVal > GetMax() ) + nTempVal = static_cast<double>(GetMax()); + else if ( nTempVal < GetMin()) + nTempVal = static_cast<double>(GetMin()); + rOutStr = CreateFieldText( static_cast<sal_Int64>(nTempVal) ); +} + +MetricFormatter::MetricFormatter(Edit* pEdit) + : NumericFormatter(pEdit) + , meUnit(FieldUnit::NONE) +{ +} + +MetricFormatter::~MetricFormatter() +{ +} + +void MetricFormatter::SetUnit( FieldUnit eNewUnit ) +{ + if (eNewUnit == FieldUnit::MM_100TH) + { + SetDecimalDigits( GetDecimalDigits() + 2 ); + meUnit = FieldUnit::MM; + } + else + meUnit = eNewUnit; + ReformatAll(); +} + +void MetricFormatter::SetCustomUnitText( const OUString& rStr ) +{ + maCustomUnitText = rStr; + ReformatAll(); +} + +void MetricFormatter::SetValue( sal_Int64 nNewValue, FieldUnit eInUnit ) +{ + SetUserValue( nNewValue, eInUnit ); +} + +OUString MetricFormatter::CreateFieldText( sal_Int64 nValue ) const +{ + //whether percent is separated from its number is locale + //specific, pawn it off to icu to decide + if (meUnit == FieldUnit::PERCENT) + { + double dValue = nValue; + dValue /= ImplPower10(GetDecimalDigits()); + return unicode::formatPercent(dValue, GetLanguageTag()); + } + + OUString aStr = NumericFormatter::CreateFieldText( nValue ); + + if( meUnit == FieldUnit::CUSTOM ) + aStr += maCustomUnitText; + else + { + OUString aSuffix = ImplMetricToString( meUnit ); + if (meUnit != FieldUnit::NONE && meUnit != FieldUnit::DEGREE && meUnit != FieldUnit::INCH && meUnit != FieldUnit::FOOT) + aStr += " "; + if (meUnit == FieldUnit::INCH) + { + OUString sDoublePrime = u"\u2033"; + if (aSuffix != "\"" && aSuffix != sDoublePrime) + aStr += " "; + else + aSuffix = sDoublePrime; + } + else if (meUnit == FieldUnit::FOOT) + { + OUString sPrime = u"\u2032"; + if (aSuffix != "'" && aSuffix != sPrime) + aStr += " "; + else + aSuffix = sPrime; + } + + assert(meUnit != FieldUnit::PERCENT); + aStr += aSuffix; + } + return aStr; +} + +void MetricFormatter::SetUserValue( sal_Int64 nNewValue, FieldUnit eInUnit ) +{ + // convert to previously configured units + nNewValue = vcl::ConvertValue( nNewValue, 0, GetDecimalDigits(), eInUnit, meUnit ); + NumericFormatter::SetUserValue( nNewValue ); +} + +sal_Int64 MetricFormatter::GetValueFromStringUnit(const OUString& rStr, FieldUnit eOutUnit) const +{ + double nTempValue; + // caution: precision loss in double cast + if (!vcl::TextToValue(rStr, nTempValue, 0, GetDecimalDigits(), ImplGetLocaleDataWrapper(), meUnit)) + nTempValue = static_cast<double>(mnLastValue); + + // caution: precision loss in double cast + if (nTempValue > mnMax) + nTempValue = static_cast<double>(mnMax); + else if (nTempValue < mnMin) + nTempValue = static_cast<double>(mnMin); + + // convert to requested units + return vcl::ConvertValue(static_cast<sal_Int64>(nTempValue), 0, GetDecimalDigits(), meUnit, eOutUnit); +} + +sal_Int64 MetricFormatter::GetValueFromString(const OUString& rStr) const +{ + return GetValueFromStringUnit(rStr, FieldUnit::NONE); +} + +sal_Int64 MetricFormatter::GetValue( FieldUnit eOutUnit ) const +{ + return GetField() ? GetValueFromStringUnit(GetField()->GetText(), eOutUnit) : 0; +} + +void MetricFormatter::SetValue( sal_Int64 nValue ) +{ + // Implementation not inline, because it is a virtual Function + SetValue( nValue, FieldUnit::NONE ); +} + +void MetricFormatter::SetMin( sal_Int64 nNewMin, FieldUnit eInUnit ) +{ + // convert to requested units + NumericFormatter::SetMin(vcl::ConvertValue(nNewMin, 0, GetDecimalDigits(), eInUnit, meUnit)); +} + +sal_Int64 MetricFormatter::GetMin( FieldUnit eOutUnit ) const +{ + // convert to requested units + return vcl::ConvertValue(NumericFormatter::GetMin(), 0, GetDecimalDigits(), meUnit, eOutUnit); +} + +void MetricFormatter::SetMax( sal_Int64 nNewMax, FieldUnit eInUnit ) +{ + // convert to requested units + NumericFormatter::SetMax(vcl::ConvertValue(nNewMax, 0, GetDecimalDigits(), eInUnit, meUnit)); +} + +sal_Int64 MetricFormatter::GetMax( FieldUnit eOutUnit ) const +{ + // convert to requested units + return vcl::ConvertValue(NumericFormatter::GetMax(), 0, GetDecimalDigits(), meUnit, eOutUnit); +} + +void MetricFormatter::Reformat() +{ + if ( !GetField() ) + return; + + OUString aText = GetField()->GetText(); + + OUString aStr; + // caution: precision loss in double cast + double nTemp = static_cast<double>(mnLastValue); + ImplMetricReformat( aText, nTemp, aStr ); + mnLastValue = static_cast<sal_Int64>(nTemp); + + if ( !aStr.isEmpty() ) + { + ImplSetText( aStr ); + } + else + SetValue( mnLastValue ); +} + +sal_Int64 MetricFormatter::GetCorrectedValue( FieldUnit eOutUnit ) const +{ + // convert to requested units + return vcl::ConvertValue(0/*nCorrectedValue*/, 0, GetDecimalDigits(), + meUnit, eOutUnit); +} + +MetricField::MetricField(vcl::Window* pParent, WinBits nWinStyle) + : SpinField(pParent, nWinStyle, WindowType::METRICFIELD) + , MetricFormatter(this) +{ + Reformat(); +} + +void MetricField::dispose() +{ + ClearField(); + SpinField::dispose(); +} + +Size MetricField::CalcMinimumSize() const +{ + return calcMinimumSize(*this, *this); +} + +bool MetricField::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "digits") + SetDecimalDigits(rValue.toInt32()); + else if (rKey == "spin-size") + SetSpinSize(rValue.toInt32()); + else + return SpinField::set_property(rKey, rValue); + return true; +} + +void MetricField::SetUnit( FieldUnit nNewUnit ) +{ + sal_Int64 nRawMax = GetMax( nNewUnit ); + sal_Int64 nMax = Denormalize( nRawMax ); + sal_Int64 nMin = Denormalize( GetMin( nNewUnit ) ); + sal_Int64 nFirst = Denormalize( GetFirst( nNewUnit ) ); + sal_Int64 nLast = Denormalize( GetLast( nNewUnit ) ); + + MetricFormatter::SetUnit( nNewUnit ); + + SetMax( Normalize( nMax ), nNewUnit ); + SetMin( Normalize( nMin ), nNewUnit ); + SetFirst( Normalize( nFirst ), nNewUnit ); + SetLast( Normalize( nLast ), nNewUnit ); +} + +void MetricField::SetFirst( sal_Int64 nNewFirst, FieldUnit eInUnit ) +{ + // convert + nNewFirst = vcl::ConvertValue(nNewFirst, 0, GetDecimalDigits(), eInUnit, meUnit); + mnFirst = nNewFirst; +} + +sal_Int64 MetricField::GetFirst( FieldUnit eOutUnit ) const +{ + // convert + return vcl::ConvertValue(mnFirst, 0, GetDecimalDigits(), meUnit, eOutUnit); +} + +void MetricField::SetLast( sal_Int64 nNewLast, FieldUnit eInUnit ) +{ + // convert + nNewLast = vcl::ConvertValue(nNewLast, 0, GetDecimalDigits(), eInUnit, meUnit); + mnLast = nNewLast; +} + +sal_Int64 MetricField::GetLast( FieldUnit eOutUnit ) const +{ + // convert + return vcl::ConvertValue(mnLast, 0, GetDecimalDigits(), meUnit, eOutUnit); +} + +bool MetricField::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplMetricProcessKeyInput( *rNEvt.GetKeyEvent(), IsUseThousandSep(), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return SpinField::PreNotify( rNEvt ); +} + +bool MetricField::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return SpinField::EventNotify( rNEvt ); +} + +void MetricField::DataChanged( const DataChangedEvent& rDCEvt ) +{ + SpinField::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + OUString sOldDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sOldThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplResetLocaleDataWrapper(); + OUString sNewDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sNewThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplUpdateSeparators( sOldDecSep, sNewDecSep, sOldThSep, sNewThSep, this ); + ReformatAll(); + } +} + +void MetricField::Modify() +{ + MarkToBeReformatted( true ); + SpinField::Modify(); +} + +void MetricField::Up() +{ + FieldUp(); + SpinField::Up(); +} + +void MetricField::Down() +{ + FieldDown(); + SpinField::Down(); +} + +void MetricField::First() +{ + FieldFirst(); + SpinField::First(); +} + +void MetricField::Last() +{ + FieldLast(); + SpinField::Last(); +} + +void MetricField::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + SpinField::DumpAsPropertyTree(rJsonWriter); + rJsonWriter.put("min", GetMin()); + rJsonWriter.put("max", GetMax()); + rJsonWriter.put("unit", FieldUnitToString(GetUnit())); + OUString sValue = Application::GetSettings().GetNeutralLocaleDataWrapper(). + getNum(GetValue(), GetDecimalDigits(), false, false); + rJsonWriter.put("value", sValue); +} + +FactoryFunction MetricField::GetUITestFactory() const +{ + return MetricFieldUIObject::create; +} + +MetricBox::MetricBox(vcl::Window* pParent, WinBits nWinStyle) + : ComboBox(pParent, nWinStyle) + , MetricFormatter(this) +{ + Reformat(); +} + +void MetricBox::dispose() +{ + ClearField(); + ComboBox::dispose(); +} + +Size MetricBox::CalcMinimumSize() const +{ + Size aRet(calcMinimumSize(*this, *this)); + + if (IsDropDownBox()) + { + Size aComboSugg(ComboBox::CalcMinimumSize()); + aRet.setWidth( std::max(aRet.Width(), aComboSugg.Width()) ); + aRet.setHeight( std::max(aRet.Height(), aComboSugg.Height()) ); + } + + return aRet; +} + +bool MetricBox::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplMetricProcessKeyInput( *rNEvt.GetKeyEvent(), IsUseThousandSep(), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return ComboBox::PreNotify( rNEvt ); +} + +bool MetricBox::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return ComboBox::EventNotify( rNEvt ); +} + +void MetricBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + ComboBox::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + OUString sOldDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sOldThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplResetLocaleDataWrapper(); + OUString sNewDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sNewThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplUpdateSeparators( sOldDecSep, sNewDecSep, sOldThSep, sNewThSep, this ); + ReformatAll(); + } +} + +void MetricBox::Modify() +{ + MarkToBeReformatted( true ); + ComboBox::Modify(); +} + +void MetricBox::ReformatAll() +{ + double nValue; + OUString aStr; + SetUpdateMode( false ); + sal_Int32 nEntryCount = GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; i++ ) + { + ImplMetricReformat( GetEntry( i ), nValue, aStr ); + RemoveEntryAt(i); + InsertEntry( aStr, i ); + } + MetricFormatter::Reformat(); + SetUpdateMode( true ); +} + +static bool ImplCurrencyProcessKeyInput( const KeyEvent& rKEvt, + bool bUseThousandSep, const LocaleDataWrapper& rWrapper ) +{ + // no strict format set; therefore allow all characters + return ImplNumericProcessKeyInput( rKEvt, false, bUseThousandSep, rWrapper ); +} + +static bool ImplCurrencyGetValue( const OUString& rStr, sal_Int64& rValue, + sal_uInt16 nDecDigits, const LocaleDataWrapper& rWrapper ) +{ + // fetch number + return ImplNumericGetValue( rStr, rValue, nDecDigits, rWrapper, true ); +} + +void CurrencyFormatter::ImplCurrencyReformat( const OUString& rStr, OUString& rOutStr ) +{ + sal_Int64 nValue; + if ( !ImplNumericGetValue( rStr, nValue, GetDecimalDigits(), ImplGetLocaleDataWrapper(), true ) ) + return; + + sal_Int64 nTempVal = nValue; + if ( nTempVal > GetMax() ) + nTempVal = GetMax(); + else if ( nTempVal < GetMin()) + nTempVal = GetMin(); + rOutStr = CreateFieldText( nTempVal ); +} + +CurrencyFormatter::CurrencyFormatter(Edit* pField) + : NumericFormatter(pField) +{ +} + +CurrencyFormatter::~CurrencyFormatter() +{ +} + +void CurrencyFormatter::SetValue( sal_Int64 nNewValue ) +{ + SetUserValue( nNewValue ); + SetEmptyFieldValueData( false ); +} + +OUString CurrencyFormatter::CreateFieldText( sal_Int64 nValue ) const +{ + return ImplGetLocaleDataWrapper().getCurr( nValue, GetDecimalDigits(), + ImplGetLocaleDataWrapper().getCurrSymbol(), + IsUseThousandSep() ); +} + +sal_Int64 CurrencyFormatter::GetValueFromString(const OUString& rStr) const +{ + sal_Int64 nTempValue; + if ( ImplCurrencyGetValue( rStr, nTempValue, GetDecimalDigits(), ImplGetLocaleDataWrapper() ) ) + { + return ClipAgainstMinMax(nTempValue); + } + else + return mnLastValue; +} + +void CurrencyFormatter::Reformat() +{ + if ( !GetField() ) + return; + + OUString aStr; + ImplCurrencyReformat( GetField()->GetText(), aStr ); + + if ( !aStr.isEmpty() ) + { + ImplSetText( aStr ); + sal_Int64 nTemp = mnLastValue; + ImplCurrencyGetValue( aStr, nTemp, GetDecimalDigits(), ImplGetLocaleDataWrapper() ); + mnLastValue = nTemp; + } + else + SetValue( mnLastValue ); +} + +CurrencyField::CurrencyField(vcl::Window* pParent, WinBits nWinStyle) + : SpinField(pParent, nWinStyle) + , CurrencyFormatter(this) +{ + Reformat(); +} + +void CurrencyField::dispose() +{ + ClearField(); + SpinField::dispose(); +} + +bool CurrencyField::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplCurrencyProcessKeyInput( *rNEvt.GetKeyEvent(), IsUseThousandSep(), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return SpinField::PreNotify( rNEvt ); +} + +bool CurrencyField::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return SpinField::EventNotify( rNEvt ); +} + +void CurrencyField::DataChanged( const DataChangedEvent& rDCEvt ) +{ + SpinField::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + OUString sOldDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sOldThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplResetLocaleDataWrapper(); + OUString sNewDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sNewThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplUpdateSeparators( sOldDecSep, sNewDecSep, sOldThSep, sNewThSep, this ); + ReformatAll(); + } +} + +void CurrencyField::Modify() +{ + MarkToBeReformatted( true ); + SpinField::Modify(); +} + +void CurrencyField::Up() +{ + FieldUp(); + SpinField::Up(); +} + +void CurrencyField::Down() +{ + FieldDown(); + SpinField::Down(); +} + +void CurrencyField::First() +{ + FieldFirst(); + SpinField::First(); +} + +void CurrencyField::Last() +{ + FieldLast(); + SpinField::Last(); +} + +CurrencyBox::CurrencyBox(vcl::Window* pParent, WinBits nWinStyle) + : ComboBox(pParent, nWinStyle) + , CurrencyFormatter(this) +{ + Reformat(); +} + +void CurrencyBox::dispose() +{ + ClearField(); + ComboBox::dispose(); +} + +bool CurrencyBox::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplCurrencyProcessKeyInput( *rNEvt.GetKeyEvent(), IsUseThousandSep(), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return ComboBox::PreNotify( rNEvt ); +} + +bool CurrencyBox::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return ComboBox::EventNotify( rNEvt ); +} + +void CurrencyBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + ComboBox::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + OUString sOldDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sOldThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplResetLocaleDataWrapper(); + OUString sNewDecSep = ImplGetLocaleDataWrapper().getNumDecimalSep(); + OUString sNewThSep = ImplGetLocaleDataWrapper().getNumThousandSep(); + ImplUpdateSeparators( sOldDecSep, sNewDecSep, sOldThSep, sNewThSep, this ); + ReformatAll(); + } +} + +void CurrencyBox::Modify() +{ + MarkToBeReformatted( true ); + ComboBox::Modify(); +} + +void CurrencyBox::ReformatAll() +{ + OUString aStr; + SetUpdateMode( false ); + sal_Int32 nEntryCount = GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; i++ ) + { + ImplCurrencyReformat( GetEntry( i ), aStr ); + RemoveEntryAt(i); + InsertEntry( aStr, i ); + } + CurrencyFormatter::Reformat(); + SetUpdateMode( true ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/field2.cxx b/vcl/source/control/field2.cxx new file mode 100644 index 000000000..b7a5c2246 --- /dev/null +++ b/vcl/source/control/field2.cxx @@ -0,0 +1,3183 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <algorithm> +#include <string_view> + +#include <tools/diagnose_ex.h> +#include <comphelper/processfactory.hxx> +#include <comphelper/string.hxx> +#include <o3tl/string_view.hxx> +#include <officecfg/Office/Common.hxx> +#include <vcl/svapp.hxx> +#include <vcl/event.hxx> +#include <vcl/toolkit/field.hxx> +#include <vcl/unohelp.hxx> +#include <vcl/settings.hxx> +#include <vcl/weldutils.hxx> + +#include <svdata.hxx> + +#include <com/sun/star/i18n/XCharacterClassification.hpp> +#include <com/sun/star/i18n/CalendarFieldIndex.hdl> + +#include <unotools/localedatawrapper.hxx> +#include <unotools/calendarwrapper.hxx> +#include <unotools/charclass.hxx> +#include <svl/numformat.hxx> +#include <svl/zforlist.hxx> + +using namespace ::com::sun::star; +using namespace ::comphelper; + +#define EDITMASK_LITERAL 'L' +#define EDITMASK_ALPHA 'a' +#define EDITMASK_UPPERALPHA 'A' +#define EDITMASK_ALPHANUM 'c' +#define EDITMASK_UPPERALPHANUM 'C' +#define EDITMASK_NUM 'N' +#define EDITMASK_NUMSPACE 'n' +#define EDITMASK_ALLCHAR 'x' +#define EDITMASK_UPPERALLCHAR 'X' + +uno::Reference< i18n::XCharacterClassification > const & ImplGetCharClass() +{ + ImplSVData *const pSVData = ImplGetSVData(); + assert(pSVData); + + if (!pSVData->m_xCharClass.is()) + { + pSVData->m_xCharClass = vcl::unohelper::CreateCharacterClassification(); + } + + return pSVData->m_xCharClass; +} + +static sal_Unicode* ImplAddString( sal_Unicode* pBuf, const OUString& rStr ) +{ + memcpy( pBuf, rStr.getStr(), rStr.getLength() * sizeof(sal_Unicode) ); + pBuf += rStr.getLength(); + return pBuf; +} + +static sal_Unicode* ImplAddNum( sal_Unicode* pBuf, sal_uLong nNumber, int nMinLen ) +{ + // fill temp buffer with digits + sal_Unicode aTempBuf[30]; + sal_Unicode* pTempBuf = aTempBuf; + do + { + *pTempBuf = static_cast<sal_Unicode>(nNumber % 10) + '0'; + pTempBuf++; + nNumber /= 10; + if ( nMinLen ) + nMinLen--; + } + while ( nNumber ); + + // fill with zeros up to the minimal length + while ( nMinLen > 0 ) + { + *pBuf = '0'; + pBuf++; + nMinLen--; + } + + // copy temp buffer to real buffer + do + { + pTempBuf--; + *pBuf = *pTempBuf; + pBuf++; + } + while ( pTempBuf != aTempBuf ); + + return pBuf; +} + +static sal_Unicode* ImplAddSNum( sal_Unicode* pBuf, sal_Int32 nNumber, int nMinLen ) +{ + if (nNumber < 0) + { + *pBuf++ = '-'; + nNumber = -nNumber; + } + return ImplAddNum( pBuf, nNumber, nMinLen); +} + +static sal_uInt16 ImplGetNum( const sal_Unicode*& rpBuf, bool& rbError ) +{ + if ( !*rpBuf ) + { + rbError = true; + return 0; + } + + sal_uInt16 nNumber = 0; + while( ( *rpBuf >= '0' ) && ( *rpBuf <= '9' ) ) + { + nNumber *= 10; + nNumber += *rpBuf - '0'; + rpBuf++; + } + + return nNumber; +} + +static void ImplSkipDelimiters( const sal_Unicode*& rpBuf ) +{ + while( ( *rpBuf == ',' ) || ( *rpBuf == '.' ) || ( *rpBuf == ';' ) || + ( *rpBuf == ':' ) || ( *rpBuf == '-' ) || ( *rpBuf == '/' ) ) + { + rpBuf++; + } +} + +static bool ImplIsPatternChar( sal_Unicode cChar, char cEditMask ) +{ + sal_Int32 nType = 0; + + try + { + OUString aCharStr(cChar); + nType = ImplGetCharClass()->getStringType( aCharStr, 0, aCharStr.getLength(), + Application::GetSettings().GetLanguageTag().getLocale() ); + } + catch (const css::uno::Exception&) + { + DBG_UNHANDLED_EXCEPTION("vcl.control"); + return false; + } + + if ( (cEditMask == EDITMASK_ALPHA) || (cEditMask == EDITMASK_UPPERALPHA) ) + { + if( !CharClass::isLetterType( nType ) ) + return false; + } + else if ( cEditMask == EDITMASK_NUM ) + { + if( !CharClass::isNumericType( nType ) ) + return false; + } + else if ( (cEditMask == EDITMASK_ALPHANUM) || (cEditMask == EDITMASK_UPPERALPHANUM) ) + { + if( !CharClass::isLetterNumericType( nType ) ) + return false; + } + else if ( (cEditMask == EDITMASK_ALLCHAR) || (cEditMask == EDITMASK_UPPERALLCHAR) ) + { + if ( cChar < 32 ) + return false; + } + else if ( cEditMask == EDITMASK_NUMSPACE ) + { + if ( !CharClass::isNumericType( nType ) && ( cChar != ' ' ) ) + return false; + } + else + return false; + + return true; +} + +static sal_Unicode ImplPatternChar( sal_Unicode cChar, char cEditMask ) +{ + if ( ImplIsPatternChar( cChar, cEditMask ) ) + { + if ( (cEditMask == EDITMASK_UPPERALPHA) || + (cEditMask == EDITMASK_UPPERALPHANUM) || + ( cEditMask == EDITMASK_UPPERALLCHAR ) ) + { + cChar = ImplGetCharClass()->toUpper(OUString(cChar), 0, 1, + Application::GetSettings().GetLanguageTag().getLocale())[0]; + } + return cChar; + } + else + return 0; +} + +static bool ImplCommaPointCharEqual( sal_Unicode c1, sal_Unicode c2 ) +{ + if ( c1 == c2 ) + return true; + else if ( ((c1 == '.') || (c1 == ',')) && + ((c2 == '.') || (c2 == ',')) ) + return true; + else + return false; +} + +static OUString ImplPatternReformat( const OUString& rStr, + const OString& rEditMask, + std::u16string_view rLiteralMask, + sal_uInt16 nFormatFlags ) +{ + if (rEditMask.isEmpty()) + return rStr; + + OUStringBuffer aOutStr(rLiteralMask); + sal_Unicode cTempChar; + sal_Unicode cChar; + sal_Unicode cLiteral; + char cMask; + sal_Int32 nStrIndex = 0; + sal_Int32 i = 0; + sal_Int32 n; + + while ( i < rEditMask.getLength() ) + { + if ( nStrIndex >= rStr.getLength() ) + break; + + cChar = rStr[nStrIndex]; + cLiteral = rLiteralMask[i]; + cMask = rEditMask[i]; + + // current position is a literal + if ( cMask == EDITMASK_LITERAL ) + { + // if it is a literal copy otherwise ignore because it might be the next valid + // character of the string + if ( ImplCommaPointCharEqual( cChar, cLiteral ) ) + nStrIndex++; + else + { + // Otherwise we check if it is an invalid character. This is the case if it does not + // fit in the pattern of the next non-literal character. + n = i+1; + while ( n < rEditMask.getLength() ) + { + if ( rEditMask[n] != EDITMASK_LITERAL ) + { + if ( !ImplIsPatternChar( cChar, rEditMask[n] ) ) + nStrIndex++; + break; + } + + n++; + } + } + } + else + { + // valid character at this position + cTempChar = ImplPatternChar( cChar, cMask ); + if ( cTempChar ) + { + // use this character + aOutStr[i] = cTempChar; + nStrIndex++; + } + else + { + // copy if it is a literal character + if ( cLiteral == cChar ) + nStrIndex++; + else + { + // If the invalid character might be the next literal character then we jump + // ahead to it, otherwise we ignore it. Do only if empty literals are allowed. + if ( nFormatFlags & PATTERN_FORMAT_EMPTYLITERALS ) + { + n = i; + while ( n < rEditMask.getLength() ) + { + if ( rEditMask[n] == EDITMASK_LITERAL ) + { + if ( ImplCommaPointCharEqual( cChar, rLiteralMask[n] ) ) + i = n+1; + + break; + } + + n++; + } + } + + nStrIndex++; + continue; + } + } + } + + i++; + } + + return aOutStr.makeStringAndClear(); +} + +static void ImplPatternMaxPos( const OUString& rStr, const OString& rEditMask, + sal_uInt16 nFormatFlags, bool bSameMask, + sal_Int32 nCursorPos, sal_Int32& rPos ) +{ + + // last position must not be longer than the contained string + sal_Int32 nMaxPos = rStr.getLength(); + + // if non empty literals are allowed ignore blanks at the end as well + if ( bSameMask && !(nFormatFlags & PATTERN_FORMAT_EMPTYLITERALS) ) + { + while ( nMaxPos ) + { + if ( (rEditMask[nMaxPos-1] != EDITMASK_LITERAL) && + (rStr[nMaxPos-1] != ' ') ) + break; + nMaxPos--; + } + + // if we are in front of a literal, continue search until first character after the literal + sal_Int32 nTempPos = nMaxPos; + while ( nTempPos < rEditMask.getLength() ) + { + if ( rEditMask[nTempPos] != EDITMASK_LITERAL ) + { + nMaxPos = nTempPos; + break; + } + nTempPos++; + } + } + + if ( rPos > nMaxPos ) + rPos = nMaxPos; + + // character should not move left + if ( rPos < nCursorPos ) + rPos = nCursorPos; +} + +static OUString ImplPatternProcessStrictModify(const OUString& rText, + const OString& rEditMask, + std::u16string_view rLiteralMask, + bool bSameMask) +{ + OUString aText(rText); + + // remove leading blanks + if (bSameMask && !rEditMask.isEmpty()) + { + sal_Int32 i = 0; + sal_Int32 nMaxLen = aText.getLength(); + while ( i < nMaxLen ) + { + if ( (rEditMask[i] != EDITMASK_LITERAL) && + (aText[i] != ' ') ) + break; + + i++; + } + // keep all literal characters + while ( i && (rEditMask[i] == EDITMASK_LITERAL) ) + i--; + aText = aText.copy( i ); + } + + return ImplPatternReformat(aText, rEditMask, rLiteralMask, 0); +} + +static void ImplPatternProcessStrictModify( Edit* pEdit, + const OString& rEditMask, + std::u16string_view rLiteralMask, + bool bSameMask ) +{ + OUString aText = pEdit->GetText(); + OUString aNewText = ImplPatternProcessStrictModify(aText, + rEditMask, + rLiteralMask, + bSameMask); + + if ( aNewText == aText ) + return; + + // adjust selection such that it remains at the end if it was there before + Selection aSel = pEdit->GetSelection(); + sal_Int64 nMaxSel = std::max( aSel.Min(), aSel.Max() ); + if ( nMaxSel >= aText.getLength() ) + { + sal_Int32 nMaxPos = aNewText.getLength(); + ImplPatternMaxPos(aNewText, rEditMask, 0, bSameMask, nMaxSel, nMaxPos); + if ( aSel.Min() == aSel.Max() ) + { + aSel.Min() = nMaxPos; + aSel.Max() = aSel.Min(); + } + else if ( aSel.Min() > aSel.Max() ) + aSel.Min() = nMaxPos; + else + aSel.Max() = nMaxPos; + } + pEdit->SetText( aNewText, aSel ); +} + +static void ImplPatternProcessStrictModify( weld::Entry& rEntry, + const OString& rEditMask, + std::u16string_view rLiteralMask, + bool bSameMask ) +{ + OUString aText = rEntry.get_text(); + OUString aNewText = ImplPatternProcessStrictModify(aText, + rEditMask, + rLiteralMask, + bSameMask); + + if (aNewText == aText) + return; + + // adjust selection such that it remains at the end if it was there before + int nStartPos, nEndPos; + rEntry.get_selection_bounds(nStartPos, nEndPos); + + int nMaxSel = std::max(nStartPos, nEndPos); + if (nMaxSel >= aText.getLength()) + { + sal_Int32 nMaxPos = aNewText.getLength(); + ImplPatternMaxPos(aNewText, rEditMask, 0, bSameMask, nMaxSel, nMaxPos); + if (nStartPos == nEndPos) + { + nStartPos = nMaxPos; + nEndPos = nMaxPos; + } + else if (nStartPos > nMaxPos) + nStartPos = nMaxPos; + else + nEndPos = nMaxPos; + } + rEntry.set_text(aNewText); + rEntry.select_region(nStartPos, nEndPos); +} + +static sal_Int32 ImplPatternLeftPos(std::string_view rEditMask, sal_Int32 nCursorPos) +{ + // search non-literal predecessor + sal_Int32 nNewPos = nCursorPos; + sal_Int32 nTempPos = nNewPos; + while ( nTempPos ) + { + if ( rEditMask[nTempPos-1] != EDITMASK_LITERAL ) + { + nNewPos = nTempPos-1; + break; + } + nTempPos--; + } + return nNewPos; +} + +static sal_Int32 ImplPatternRightPos( const OUString& rStr, const OString& rEditMask, + sal_uInt16 nFormatFlags, bool bSameMask, + sal_Int32 nCursorPos ) +{ + // search non-literal successor + sal_Int32 nNewPos = nCursorPos; + ; + for(sal_Int32 nTempPos = nNewPos+1; nTempPos < rEditMask.getLength(); ++nTempPos ) + { + if ( rEditMask[nTempPos] != EDITMASK_LITERAL ) + { + nNewPos = nTempPos; + break; + } + } + ImplPatternMaxPos( rStr, rEditMask, nFormatFlags, bSameMask, nCursorPos, nNewPos ); + return nNewPos; +} + +namespace +{ + class IEditImplementation + { + public: + virtual ~IEditImplementation() {} + + virtual OUString GetText() const = 0; + virtual void SetText(const OUString& rStr, const Selection& rSelection) = 0; + + virtual Selection GetSelection() const = 0; + virtual void SetSelection(const Selection& rSelection) = 0; + + virtual bool IsInsertMode() const = 0; + + virtual void SetModified() = 0; + }; +} + +static bool ImplPatternProcessKeyInput( IEditImplementation& rEdit, const KeyEvent& rKEvt, + const OString& rEditMask, + std::u16string_view rLiteralMask, + bool bStrictFormat, + bool bSameMask, + bool& rbInKeyInput ) +{ + if ( rEditMask.isEmpty() || !bStrictFormat ) + return false; + + sal_uInt16 nFormatFlags = 0; + Selection aOldSel = rEdit.GetSelection(); + vcl::KeyCode aCode = rKEvt.GetKeyCode(); + sal_Unicode cChar = rKEvt.GetCharCode(); + sal_uInt16 nKeyCode = aCode.GetCode(); + bool bShift = aCode.IsShift(); + sal_Int32 nCursorPos = static_cast<sal_Int32>(aOldSel.Max()); + sal_Int32 nNewPos; + sal_Int32 nTempPos; + + if ( nKeyCode && !aCode.IsMod1() && !aCode.IsMod2() ) + { + if ( nKeyCode == KEY_LEFT ) + { + Selection aSel( ImplPatternLeftPos( rEditMask, nCursorPos ) ); + if ( bShift ) + aSel.Min() = aOldSel.Min(); + rEdit.SetSelection( aSel ); + return true; + } + else if ( nKeyCode == KEY_RIGHT ) + { + // Use the start of selection as minimum; even a small position is allowed in case that + // all was selected by the focus + Selection aSel( aOldSel ); + aSel.Justify(); + nCursorPos = aSel.Min(); + aSel.Max() = ImplPatternRightPos( rEdit.GetText(), rEditMask, nFormatFlags, bSameMask, nCursorPos ); + if ( bShift ) + aSel.Min() = aOldSel.Min(); + else + aSel.Min() = aSel.Max(); + rEdit.SetSelection( aSel ); + return true; + } + else if ( nKeyCode == KEY_HOME ) + { + // Home is the position of the first non-literal character + nNewPos = 0; + while ( (nNewPos < rEditMask.getLength()) && + (rEditMask[nNewPos] == EDITMASK_LITERAL) ) + nNewPos++; + + // Home should not move to the right + if ( nCursorPos < nNewPos ) + nNewPos = nCursorPos; + Selection aSel( nNewPos ); + if ( bShift ) + aSel.Min() = aOldSel.Min(); + rEdit.SetSelection( aSel ); + return true; + } + else if ( nKeyCode == KEY_END ) + { + // End is position of last non-literal character + nNewPos = rEditMask.getLength(); + while ( nNewPos && + (rEditMask[nNewPos-1] == EDITMASK_LITERAL) ) + nNewPos--; + // Use the start of selection as minimum; even a small position is allowed in case that + // all was selected by the focus + Selection aSel( aOldSel ); + aSel.Justify(); + nCursorPos = static_cast<sal_Int32>(aSel.Min()); + ImplPatternMaxPos( rEdit.GetText(), rEditMask, nFormatFlags, bSameMask, nCursorPos, nNewPos ); + aSel.Max() = nNewPos; + if ( bShift ) + aSel.Min() = aOldSel.Min(); + else + aSel.Min() = aSel.Max(); + rEdit.SetSelection( aSel ); + return true; + } + else if ( (nKeyCode == KEY_BACKSPACE) || (nKeyCode == KEY_DELETE) ) + { + OUString aOldStr( rEdit.GetText() ); + OUStringBuffer aStr( aOldStr ); + Selection aSel = aOldSel; + + aSel.Justify(); + nNewPos = static_cast<sal_Int32>(aSel.Min()); + + // if selection then delete it + if ( aSel.Len() ) + { + if ( bSameMask ) + aStr.remove( static_cast<sal_Int32>(aSel.Min()), static_cast<sal_Int32>(aSel.Len()) ); + else + { + std::u16string_view aRep = rLiteralMask.substr( static_cast<sal_Int32>(aSel.Min()), static_cast<sal_Int32>(aSel.Len()) ); + aStr.remove( aSel.Min(), aRep.size() ); + aStr.insert( aSel.Min(), aRep ); + } + } + else + { + if ( nKeyCode == KEY_BACKSPACE ) + { + nTempPos = nNewPos; + nNewPos = ImplPatternLeftPos( rEditMask, nTempPos ); + } + else + nTempPos = ImplPatternRightPos( aStr.toString(), rEditMask, nFormatFlags, bSameMask, nNewPos ); + + if ( nNewPos != nTempPos ) + { + if ( bSameMask ) + { + if ( rEditMask[nNewPos] != EDITMASK_LITERAL ) + aStr.remove( nNewPos, 1 ); + } + else + { + aStr[nNewPos] = rLiteralMask[nNewPos]; + } + } + } + + OUString sStr = aStr.makeStringAndClear(); + if ( aOldStr != sStr ) + { + if ( bSameMask ) + sStr = ImplPatternReformat( sStr, rEditMask, rLiteralMask, nFormatFlags ); + rbInKeyInput = true; + rEdit.SetText( sStr, Selection( nNewPos ) ); + rEdit.SetModified(); + rbInKeyInput = false; + } + else + rEdit.SetSelection( Selection( nNewPos ) ); + + return true; + } + else if ( nKeyCode == KEY_INSERT ) + { + // you can only set InsertMode for a PatternField if the + // mask is equal at all input positions + if ( !bSameMask ) + { + return true; + } + } + } + + if ( rKEvt.GetKeyCode().IsMod2() || (cChar < 32) || (cChar == 127) ) + return false; + + Selection aSel = aOldSel; + aSel.Justify(); + nNewPos = aSel.Min(); + + if ( nNewPos < rEditMask.getLength() ) + { + sal_Unicode cPattChar = ImplPatternChar( cChar, rEditMask[nNewPos] ); + if ( cPattChar ) + cChar = cPattChar; + else + { + // If no valid character, check if the user wanted to jump to next literal. We do this + // only if we're after a character, so that literals that were skipped automatically + // do not influence the position anymore. + if ( nNewPos && + (rEditMask[nNewPos-1] != EDITMASK_LITERAL) && + !aSel.Len() ) + { + // search for next character not being a literal + nTempPos = nNewPos; + while ( nTempPos < rEditMask.getLength() ) + { + if ( rEditMask[nTempPos] == EDITMASK_LITERAL ) + { + // only valid if no literal present + if ( (rEditMask[nTempPos+1] != EDITMASK_LITERAL ) && + ImplCommaPointCharEqual( cChar, rLiteralMask[nTempPos] ) ) + { + nTempPos++; + ImplPatternMaxPos( rEdit.GetText(), rEditMask, nFormatFlags, bSameMask, nNewPos, nTempPos ); + if ( nTempPos > nNewPos ) + { + rEdit.SetSelection( Selection( nTempPos ) ); + return true; + } + } + break; + } + nTempPos++; + } + } + + cChar = 0; + } + } + else + cChar = 0; + if ( cChar ) + { + OUStringBuffer aStr(rEdit.GetText()); + bool bError = false; + if ( bSameMask && rEdit.IsInsertMode() ) + { + // crop spaces and literals at the end until current position + sal_Int32 n = aStr.getLength(); + while ( n && (n > nNewPos) ) + { + if ( (aStr[n-1] != ' ') && + ((n > rEditMask.getLength()) || (rEditMask[n-1] != EDITMASK_LITERAL)) ) + break; + + n--; + } + aStr.truncate( n ); + + if ( aSel.Len() ) + aStr.remove( aSel.Min(), aSel.Len() ); + + if ( aStr.getLength() < rEditMask.getLength() ) + { + // possibly extend string until cursor position + if ( aStr.getLength() < nNewPos ) + aStr.append( rLiteralMask.substr(aStr.getLength(), nNewPos-aStr.getLength()) ); + if ( nNewPos < aStr.getLength() ) + aStr.insert( cChar, nNewPos ); + else if ( nNewPos < rEditMask.getLength() ) + aStr.append(cChar); + aStr = ImplPatternReformat( aStr.toString(), rEditMask, rLiteralMask, nFormatFlags ); + } + else + bError = true; + } + else + { + if ( aSel.Len() ) + { + // delete selection + std::u16string_view aRep = rLiteralMask.substr( aSel.Min(), aSel.Len() ); + aStr.remove( aSel.Min(), aRep.size() ); + aStr.insert( aSel.Min(), aRep ); + } + + if ( nNewPos < aStr.getLength() ) + aStr[nNewPos] = cChar; + else if ( nNewPos < rEditMask.getLength() ) + aStr.append(cChar); + } + + if ( !bError ) + { + rbInKeyInput = true; + const OUString sStr = aStr.makeStringAndClear(); + Selection aNewSel( ImplPatternRightPos( sStr, rEditMask, nFormatFlags, bSameMask, nNewPos ) ); + rEdit.SetText( sStr, aNewSel ); + rEdit.SetModified(); + rbInKeyInput = false; + } + } + + return true; +} + +namespace +{ + bool ImplSetMask(const OString& rEditMask, OUString& rLiteralMask) + { + bool bSameMask = true; + + if (rEditMask.getLength() != rLiteralMask.getLength()) + { + OUStringBuffer aBuf(rLiteralMask); + if (rEditMask.getLength() < aBuf.getLength()) + aBuf.setLength(rEditMask.getLength()); + else + comphelper::string::padToLength(aBuf, rEditMask.getLength(), ' '); + rLiteralMask = aBuf.makeStringAndClear(); + } + + // Strict mode allows only the input mode if only equal characters are allowed as mask and if + // only spaces are specified which are not allowed by the mask + sal_Int32 i = 0; + char c = 0; + while ( i < rEditMask.getLength() ) + { + char cTemp = rEditMask[i]; + if ( cTemp != EDITMASK_LITERAL ) + { + if ( (cTemp == EDITMASK_ALLCHAR) || + (cTemp == EDITMASK_UPPERALLCHAR) || + (cTemp == EDITMASK_NUMSPACE) ) + { + bSameMask = false; + break; + } + if ( i < rLiteralMask.getLength() ) + { + if ( rLiteralMask[i] != ' ' ) + { + bSameMask = false; + break; + } + } + if ( !c ) + c = cTemp; + if ( cTemp != c ) + { + bSameMask = false; + break; + } + } + i++; + } + + return bSameMask; + } +} + +PatternFormatter::PatternFormatter(Edit* pEdit) + : FormatterBase(pEdit) +{ + mbSameMask = true; + mbInPattKeyInput = false; +} + +PatternFormatter::~PatternFormatter() +{ +} + +void PatternFormatter::SetMask( const OString& rEditMask, + const OUString& rLiteralMask ) +{ + m_aEditMask = rEditMask; + maLiteralMask = rLiteralMask; + mbSameMask = ImplSetMask(m_aEditMask, maLiteralMask); + ReformatAll(); +} + +namespace +{ + class EntryImplementation : public IEditImplementation + { + public: + EntryImplementation(weld::PatternFormatter& rFormatter) + : m_rFormatter(rFormatter) + , m_rEntry(rFormatter.get_widget()) + { + } + + virtual OUString GetText() const override + { + return m_rEntry.get_text(); + } + + virtual void SetText(const OUString& rStr, const Selection& rSelection) override + { + m_rEntry.set_text(rStr); + SetSelection(rSelection); + } + + virtual Selection GetSelection() const override + { + int nStartPos, nEndPos; + m_rEntry.get_selection_bounds(nStartPos, nEndPos); + return Selection(nStartPos, nEndPos); + } + + virtual void SetSelection(const Selection& rSelection) override + { + auto nMin = rSelection.Min(); + auto nMax = rSelection.Max(); + m_rEntry.select_region(nMin < 0 ? 0 : nMin, nMax == SELECTION_MAX ? -1 : nMax); + } + + virtual bool IsInsertMode() const override + { + return !m_rEntry.get_overwrite_mode(); + } + + virtual void SetModified() override + { + m_rFormatter.Modify(); + } + + private: + weld::PatternFormatter& m_rFormatter; + weld::Entry& m_rEntry; + }; +} + +namespace weld +{ + void PatternFormatter::SetStrictFormat(bool bStrict) + { + if (bStrict != m_bStrictFormat) + { + m_bStrictFormat = bStrict; + if (m_bStrictFormat) + ReformatAll(); + } + } + + void PatternFormatter::SetMask(const OString& rEditMask, + const OUString& rLiteralMask) + { + m_aEditMask = rEditMask; + m_aLiteralMask = rLiteralMask; + m_bSameMask = ImplSetMask(m_aEditMask, m_aLiteralMask); + ReformatAll(); + } + + void PatternFormatter::ReformatAll() + { + m_rEntry.set_text(ImplPatternReformat(m_rEntry.get_text(), m_aEditMask, m_aLiteralMask, 0/*nFormatFlags*/)); + if (!m_bSameMask && m_bStrictFormat && m_rEntry.get_editable()) + m_rEntry.set_overwrite_mode(true); + } + + void PatternFormatter::EntryGainFocus() + { + m_bReformat = false; + } + + void PatternFormatter::EntryLostFocus() + { + if (m_bReformat) + ReformatAll(); + } + + void PatternFormatter::Modify() + { + if (!m_bInPattKeyInput) + { + if (m_bStrictFormat) + ImplPatternProcessStrictModify(m_rEntry, m_aEditMask, m_aLiteralMask, m_bSameMask); + else + m_bReformat = true; + } + m_aModifyHdl.Call(m_rEntry); + } + + IMPL_LINK(PatternFormatter, KeyInputHdl, const KeyEvent&, rKEvt, bool) + { + if (m_aKeyPressHdl.Call(rKEvt)) + return true; + if (rKEvt.GetKeyCode().IsMod2()) + return false; + EntryImplementation aAdapt(*this); + return ImplPatternProcessKeyInput(aAdapt, rKEvt, m_aEditMask, m_aLiteralMask, + m_bStrictFormat, + m_bSameMask, m_bInPattKeyInput); + } +} + +void PatternFormatter::SetString( const OUString& rStr ) +{ + if ( GetField() ) + { + GetField()->SetText( rStr ); + MarkToBeReformatted( false ); + } +} + +OUString PatternFormatter::GetString() const +{ + if ( !GetField() ) + return OUString(); + else + return ImplPatternReformat( GetField()->GetText(), m_aEditMask, maLiteralMask, 0/*nFormatFlags*/ ); +} + +void PatternFormatter::Reformat() +{ + if ( GetField() ) + { + ImplSetText( ImplPatternReformat( GetField()->GetText(), m_aEditMask, maLiteralMask, 0/*nFormatFlags*/ ) ); + if ( !mbSameMask && IsStrictFormat() && !GetField()->IsReadOnly() ) + GetField()->SetInsertMode( false ); + } +} + +PatternField::PatternField(vcl::Window* pParent, WinBits nWinStyle) + : SpinField(pParent, nWinStyle) + , PatternFormatter(this) +{ + Reformat(); +} + +void PatternField::dispose() +{ + ClearField(); + SpinField::dispose(); +} + +namespace +{ + class EditImplementation : public IEditImplementation + { + public: + EditImplementation(Edit& rEdit) + : m_rEdit(rEdit) + { + } + + virtual OUString GetText() const override + { + return m_rEdit.GetText(); + } + + virtual void SetText(const OUString& rStr, const Selection& rSelection) override + { + m_rEdit.SetText(rStr, rSelection); + } + + virtual Selection GetSelection() const override + { + return m_rEdit.GetSelection(); + } + + virtual void SetSelection(const Selection& rSelection) override + { + m_rEdit.SetSelection(rSelection); + } + + virtual bool IsInsertMode() const override + { + return m_rEdit.IsInsertMode(); + } + + virtual void SetModified() override + { + m_rEdit.SetModifyFlag(); + m_rEdit.Modify(); + } + + private: + Edit& m_rEdit; + }; +} + +bool PatternField::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + EditImplementation aAdapt(*GetField()); + if ( ImplPatternProcessKeyInput( aAdapt, *rNEvt.GetKeyEvent(), GetEditMask(), GetLiteralMask(), + IsStrictFormat(), + ImplIsSameMask(), ImplGetInPattKeyInput() ) ) + return true; + } + + return SpinField::PreNotify( rNEvt ); +} + +bool PatternField::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return SpinField::EventNotify( rNEvt ); +} + +void PatternField::Modify() +{ + if ( !ImplGetInPattKeyInput() ) + { + if ( IsStrictFormat() ) + ImplPatternProcessStrictModify( GetField(), GetEditMask(), GetLiteralMask(), ImplIsSameMask() ); + else + MarkToBeReformatted( true ); + } + + SpinField::Modify(); +} + +PatternBox::PatternBox(vcl::Window* pParent, WinBits nWinStyle) + : ComboBox( pParent, nWinStyle ) + , PatternFormatter(this) +{ + Reformat(); +} + +void PatternBox::dispose() +{ + ClearField(); + ComboBox::dispose(); +} + +bool PatternBox::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + EditImplementation aAdapt(*GetField()); + if ( ImplPatternProcessKeyInput( aAdapt, *rNEvt.GetKeyEvent(), GetEditMask(), GetLiteralMask(), + IsStrictFormat(), + ImplIsSameMask(), ImplGetInPattKeyInput() ) ) + return true; + } + + return ComboBox::PreNotify( rNEvt ); +} + +bool PatternBox::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return ComboBox::EventNotify( rNEvt ); +} + +void PatternBox::Modify() +{ + if ( !ImplGetInPattKeyInput() ) + { + if ( IsStrictFormat() ) + ImplPatternProcessStrictModify( GetField(), GetEditMask(), GetLiteralMask(), ImplIsSameMask() ); + else + MarkToBeReformatted( true ); + } + + ComboBox::Modify(); +} + +void PatternBox::ReformatAll() +{ + OUString aStr; + SetUpdateMode( false ); + const sal_Int32 nEntryCount = GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; ++i ) + { + aStr = ImplPatternReformat( GetEntry( i ), GetEditMask(), GetLiteralMask(), 0/*nFormatFlags*/ ); + RemoveEntryAt(i); + InsertEntry( aStr, i ); + } + PatternFormatter::Reformat(); + SetUpdateMode( true ); +} + +static ExtDateFieldFormat ImplGetExtFormat( LongDateOrder eOld ) +{ + switch( eOld ) + { + case LongDateOrder::YDM: + case LongDateOrder::DMY: return ExtDateFieldFormat::ShortDDMMYY; + case LongDateOrder::MDY: return ExtDateFieldFormat::ShortMMDDYY; + case LongDateOrder::YMD: + default: return ExtDateFieldFormat::ShortYYMMDD; + } +} + +static sal_uInt16 ImplCutNumberFromString( OUString& rStr ) +{ + sal_Int32 i1 = 0; + while (i1 != rStr.getLength() && (rStr[i1] < '0' || rStr[i1] > '9')) { + ++i1; + } + sal_Int32 i2 = i1; + while (i2 != rStr.getLength() && rStr[i2] >= '0' && rStr[i2] <= '9') { + ++i2; + } + sal_Int32 nValue = o3tl::toInt32(rStr.subView(i1, i2-i1)); + rStr = rStr.copy(std::min(i2+1, rStr.getLength())); + return nValue; +} + +static bool ImplCutMonthName( OUString& rStr, std::u16string_view _rLookupMonthName ) +{ + sal_Int32 index = 0; + rStr = rStr.replaceFirst(_rLookupMonthName, "", &index); + return index >= 0; +} + +static sal_uInt16 ImplGetMonthFromCalendarItem( OUString& rStr, const uno::Sequence< i18n::CalendarItem2 >& rMonths ) +{ + const sal_uInt16 nMonths = rMonths.getLength(); + for (sal_uInt16 i=0; i < nMonths; ++i) + { + // long month name? + if ( ImplCutMonthName( rStr, rMonths[i].FullName ) ) + return i+1; + + // short month name? + if ( ImplCutMonthName( rStr, rMonths[i].AbbrevName ) ) + return i+1; + } + return 0; +} + +static sal_uInt16 ImplCutMonthFromString( OUString& rStr, OUString& rCalendarName, + const LocaleDataWrapper& rLocaleData, const CalendarWrapper& rCalendarWrapper ) +{ + const OUString aDefaultCalendarName( rCalendarWrapper.getUniqueID()); + rCalendarName = aDefaultCalendarName; + + // Search for a month name of the loaded default calendar. + const uno::Sequence< i18n::CalendarItem2 > aMonths = rCalendarWrapper.getMonths(); + sal_uInt16 nMonth = ImplGetMonthFromCalendarItem( rStr, aMonths); + if (nMonth > 0) + return nMonth; + + // And also possessive genitive and partitive month names. + const uno::Sequence< i18n::CalendarItem2 > aGenitiveMonths = rCalendarWrapper.getGenitiveMonths(); + if (aGenitiveMonths != aMonths) + { + nMonth = ImplGetMonthFromCalendarItem( rStr, aGenitiveMonths); + if (nMonth > 0) + return nMonth; + } + const uno::Sequence< i18n::CalendarItem2 > aPartitiveMonths = rCalendarWrapper.getPartitiveMonths(); + if (aPartitiveMonths != aMonths) + { + nMonth = ImplGetMonthFromCalendarItem( rStr, aPartitiveMonths); + if (nMonth > 0) + return nMonth; + } + + // Check if there are more calendars and try them if so, as the long date + // format is obtained from the number formatter this is possible (e.g. + // ar_DZ "[~hijri] ...") + const uno::Sequence< i18n::Calendar2 > aCalendars = rLocaleData.getAllCalendars(); + if (aCalendars.getLength() > 1) + { + for (const auto& rCalendar : aCalendars) + { + if (rCalendar.Name != aDefaultCalendarName) + { + rCalendarName = rCalendar.Name; + + nMonth = ImplGetMonthFromCalendarItem( rStr, rCalendar.Months); + if (nMonth > 0) + return nMonth; + + if (rCalendar.Months != rCalendar.GenitiveMonths) + { + nMonth = ImplGetMonthFromCalendarItem( rStr, rCalendar.GenitiveMonths); + if (nMonth > 0) + return nMonth; + } + + if (rCalendar.Months != rCalendar.PartitiveMonths) + { + nMonth = ImplGetMonthFromCalendarItem( rStr, rCalendar.PartitiveMonths); + if (nMonth > 0) + return nMonth; + } + + rCalendarName = aDefaultCalendarName; + } + } + } + + return ImplCutNumberFromString( rStr ); +} + +static OUString ImplGetDateSep( const LocaleDataWrapper& rLocaleDataWrapper, ExtDateFieldFormat eFormat ) +{ + if ( ( eFormat == ExtDateFieldFormat::ShortYYMMDD_DIN5008 ) || ( eFormat == ExtDateFieldFormat::ShortYYYYMMDD_DIN5008 ) ) + return "-"; + else + return rLocaleDataWrapper.getDateSep(); +} + +static bool ImplDateProcessKeyInput( const KeyEvent& rKEvt, ExtDateFieldFormat eFormat, + const LocaleDataWrapper& rLocaleDataWrapper ) +{ + sal_Unicode cChar = rKEvt.GetCharCode(); + sal_uInt16 nGroup = rKEvt.GetKeyCode().GetGroup(); + return !((nGroup == KEYGROUP_FKEYS) || + (nGroup == KEYGROUP_CURSOR) || + (nGroup == KEYGROUP_MISC)|| + ((cChar >= '0') && (cChar <= '9')) || + (cChar == ImplGetDateSep( rLocaleDataWrapper, eFormat )[0])); +} + +bool DateFormatter::TextToDate(const OUString& rStr, Date& rDate, ExtDateFieldFormat eDateOrder, + const LocaleDataWrapper& rLocaleDataWrapper, const CalendarWrapper& rCalendarWrapper) +{ + sal_uInt16 nDay = 0; + sal_uInt16 nMonth = 0; + sal_uInt16 nYear = 0; + bool bError = false; + OUString aStr( rStr ); + + if ( eDateOrder == ExtDateFieldFormat::SystemLong ) + { + OUString aCalendarName; + LongDateOrder eFormat = rLocaleDataWrapper.getLongDateOrder(); + switch( eFormat ) + { + case LongDateOrder::MDY: + nMonth = ImplCutMonthFromString( aStr, aCalendarName, rLocaleDataWrapper, rCalendarWrapper ); + nDay = ImplCutNumberFromString( aStr ); + nYear = ImplCutNumberFromString( aStr ); + break; + case LongDateOrder::DMY: + nDay = ImplCutNumberFromString( aStr ); + nMonth = ImplCutMonthFromString( aStr, aCalendarName, rLocaleDataWrapper, rCalendarWrapper ); + nYear = ImplCutNumberFromString( aStr ); + break; + case LongDateOrder::YDM: + nYear = ImplCutNumberFromString( aStr ); + nDay = ImplCutNumberFromString( aStr ); + nMonth = ImplCutMonthFromString( aStr, aCalendarName, rLocaleDataWrapper, rCalendarWrapper ); + break; + case LongDateOrder::YMD: + default: + nYear = ImplCutNumberFromString( aStr ); + nMonth = ImplCutMonthFromString( aStr, aCalendarName, rLocaleDataWrapper, rCalendarWrapper ); + nDay = ImplCutNumberFromString( aStr ); + break; + } + if (aCalendarName != "gregorian") + { + // Calendar widget is Gregorian, convert date. + // Need full date. + bError = !nDay || !nMonth || !nYear; + if (!bError) + { + CalendarWrapper aCW( rLocaleDataWrapper.getComponentContext()); + aCW.loadCalendar( aCalendarName, rLocaleDataWrapper.getLoadedLanguageTag().getLocale()); + aCW.setDateTime(0.5); // get rid of current time, set some day noon + aCW.setValue( i18n::CalendarFieldIndex::DAY_OF_MONTH, nDay); + aCW.setValue( i18n::CalendarFieldIndex::MONTH, nMonth - 1); + aCW.setValue( i18n::CalendarFieldIndex::YEAR, nYear); + bError = !aCW.isValid(); + if (!bError) + { + Date aDate = aCW.getEpochStart() + aCW.getDateTime(); + nYear = aDate.GetYear(); + nMonth = aDate.GetMonth(); + nDay = aDate.GetDay(); + } + } + } + } + else + { + bool bYear = true; + + // Check if year is present: + OUString aDateSep = ImplGetDateSep( rLocaleDataWrapper, eDateOrder ); + sal_Int32 nSepPos = aStr.indexOf( aDateSep ); + if ( nSepPos < 0 ) + return false; + nSepPos = aStr.indexOf( aDateSep, nSepPos+1 ); + if ( ( nSepPos < 0 ) || ( nSepPos == (aStr.getLength()-1) ) ) + { + bYear = false; + nYear = Date( Date::SYSTEM ).GetYearUnsigned(); + } + + const sal_Unicode* pBuf = aStr.getStr(); + ImplSkipDelimiters( pBuf ); + + switch ( eDateOrder ) + { + case ExtDateFieldFormat::ShortDDMMYY: + case ExtDateFieldFormat::ShortDDMMYYYY: + { + nDay = ImplGetNum( pBuf, bError ); + ImplSkipDelimiters( pBuf ); + nMonth = ImplGetNum( pBuf, bError ); + ImplSkipDelimiters( pBuf ); + if ( bYear ) + nYear = ImplGetNum( pBuf, bError ); + } + break; + case ExtDateFieldFormat::ShortMMDDYY: + case ExtDateFieldFormat::ShortMMDDYYYY: + { + nMonth = ImplGetNum( pBuf, bError ); + ImplSkipDelimiters( pBuf ); + nDay = ImplGetNum( pBuf, bError ); + ImplSkipDelimiters( pBuf ); + if ( bYear ) + nYear = ImplGetNum( pBuf, bError ); + } + break; + case ExtDateFieldFormat::ShortYYMMDD: + case ExtDateFieldFormat::ShortYYYYMMDD: + case ExtDateFieldFormat::ShortYYMMDD_DIN5008: + case ExtDateFieldFormat::ShortYYYYMMDD_DIN5008: + { + if ( bYear ) + nYear = ImplGetNum( pBuf, bError ); + ImplSkipDelimiters( pBuf ); + nMonth = ImplGetNum( pBuf, bError ); + ImplSkipDelimiters( pBuf ); + nDay = ImplGetNum( pBuf, bError ); + } + break; + + default: + { + OSL_FAIL( "DateOrder???" ); + } + } + } + + if ( bError || !nDay || !nMonth ) + return false; + + Date aNewDate( nDay, nMonth, nYear ); + DateFormatter::ExpandCentury( aNewDate, officecfg::Office::Common::DateFormat::TwoDigitYear::get() ); + if ( aNewDate.IsValidDate() ) + { + rDate = aNewDate; + return true; + } + return false; +} + +void DateFormatter::ImplDateReformat( const OUString& rStr, OUString& rOutStr ) +{ + Date aDate( Date::EMPTY ); + if (!TextToDate(rStr, aDate, GetExtDateFormat(true), ImplGetLocaleDataWrapper(), GetCalendarWrapper())) + return; + + Date aTempDate = aDate; + if ( aTempDate > GetMax() ) + aTempDate = GetMax(); + else if ( aTempDate < GetMin() ) + aTempDate = GetMin(); + + rOutStr = ImplGetDateAsText( aTempDate ); +} + +namespace +{ + ExtDateFieldFormat ResolveSystemFormat(ExtDateFieldFormat eDateFormat, const LocaleDataWrapper& rLocaleData) + { + if (eDateFormat <= ExtDateFieldFormat::SystemShortYYYY) + { + bool bShowCentury = (eDateFormat == ExtDateFieldFormat::SystemShortYYYY); + switch (rLocaleData.getDateOrder()) + { + case DateOrder::DMY: + eDateFormat = bShowCentury ? ExtDateFieldFormat::ShortDDMMYYYY : ExtDateFieldFormat::ShortDDMMYY; + break; + case DateOrder::MDY: + eDateFormat = bShowCentury ? ExtDateFieldFormat::ShortMMDDYYYY : ExtDateFieldFormat::ShortMMDDYY; + break; + default: + eDateFormat = bShowCentury ? ExtDateFieldFormat::ShortYYYYMMDD : ExtDateFieldFormat::ShortYYMMDD; + } + } + return eDateFormat; + } +} + +OUString DateFormatter::FormatDate(const Date& rDate, ExtDateFieldFormat eExtFormat, + const LocaleDataWrapper& rLocaleData, + const Formatter::StaticFormatter& rStaticFormatter) +{ + bool bShowCentury = false; + switch (eExtFormat) + { + case ExtDateFieldFormat::SystemShortYYYY: + case ExtDateFieldFormat::SystemLong: + case ExtDateFieldFormat::ShortDDMMYYYY: + case ExtDateFieldFormat::ShortMMDDYYYY: + case ExtDateFieldFormat::ShortYYYYMMDD: + case ExtDateFieldFormat::ShortYYYYMMDD_DIN5008: + { + bShowCentury = true; + } + break; + default: + { + bShowCentury = false; + } + } + + if ( !bShowCentury ) + { + // Check if I have to use force showing the century + sal_uInt16 nTwoDigitYearStart = officecfg::Office::Common::DateFormat::TwoDigitYear::get(); + sal_uInt16 nYear = rDate.GetYearUnsigned(); + + // If year is not in double digit range + if ( (nYear < nTwoDigitYearStart) || (nYear >= nTwoDigitYearStart+100) ) + bShowCentury = true; + } + + sal_Unicode aBuf[128]; + sal_Unicode* pBuf = aBuf; + + eExtFormat = ResolveSystemFormat(eExtFormat, rLocaleData); + + OUString aDateSep = ImplGetDateSep( rLocaleData, eExtFormat ); + sal_uInt16 nDay = rDate.GetDay(); + sal_uInt16 nMonth = rDate.GetMonth(); + sal_Int16 nYear = rDate.GetYear(); + sal_uInt16 nYearLen = bShowCentury ? 4 : 2; + + if ( !bShowCentury ) + nYear %= 100; + + switch (eExtFormat) + { + case ExtDateFieldFormat::SystemLong: + { + SvNumberFormatter* pFormatter = rStaticFormatter; + const LanguageTag aFormatterLang( pFormatter->GetLanguageTag()); + const sal_uInt32 nIndex = pFormatter->GetFormatIndex( NF_DATE_SYSTEM_LONG, + rLocaleData.getLanguageTag().getLanguageType(false)); + OUString aStr; + const Color* pCol; + pFormatter->GetOutputString( rDate - pFormatter->GetNullDate(), nIndex, aStr, &pCol); + // Reset to what other uses may expect. + pFormatter->ChangeIntl( aFormatterLang.getLanguageType(false)); + return aStr; + } + case ExtDateFieldFormat::ShortDDMMYY: + case ExtDateFieldFormat::ShortDDMMYYYY: + { + pBuf = ImplAddNum( pBuf, nDay, 2 ); + pBuf = ImplAddString( pBuf, aDateSep ); + pBuf = ImplAddNum( pBuf, nMonth, 2 ); + pBuf = ImplAddString( pBuf, aDateSep ); + pBuf = ImplAddSNum( pBuf, nYear, nYearLen ); + } + break; + case ExtDateFieldFormat::ShortMMDDYY: + case ExtDateFieldFormat::ShortMMDDYYYY: + { + pBuf = ImplAddNum( pBuf, nMonth, 2 ); + pBuf = ImplAddString( pBuf, aDateSep ); + pBuf = ImplAddNum( pBuf, nDay, 2 ); + pBuf = ImplAddString( pBuf, aDateSep ); + pBuf = ImplAddSNum( pBuf, nYear, nYearLen ); + } + break; + case ExtDateFieldFormat::ShortYYMMDD: + case ExtDateFieldFormat::ShortYYYYMMDD: + case ExtDateFieldFormat::ShortYYMMDD_DIN5008: + case ExtDateFieldFormat::ShortYYYYMMDD_DIN5008: + { + pBuf = ImplAddSNum( pBuf, nYear, nYearLen ); + pBuf = ImplAddString( pBuf, aDateSep ); + pBuf = ImplAddNum( pBuf, nMonth, 2 ); + pBuf = ImplAddString( pBuf, aDateSep ); + pBuf = ImplAddNum( pBuf, nDay, 2 ); + } + break; + default: + { + OSL_FAIL( "DateOrder???" ); + } + } + + return OUString(aBuf, pBuf-aBuf); +} + +OUString DateFormatter::ImplGetDateAsText( const Date& rDate ) const +{ + return DateFormatter::FormatDate(rDate, GetExtDateFormat(), ImplGetLocaleDataWrapper(), maStaticFormatter); +} + +static void ImplDateIncrementDay( Date& rDate, bool bUp ) +{ + DateFormatter::ExpandCentury( rDate ); + rDate.AddDays( bUp ? 1 : -1 ); +} + +static void ImplDateIncrementMonth( Date& rDate, bool bUp ) +{ + DateFormatter::ExpandCentury( rDate ); + rDate.AddMonths( bUp ? 1 : -1 ); +} + +static void ImplDateIncrementYear( Date& rDate, bool bUp ) +{ + DateFormatter::ExpandCentury( rDate ); + rDate.AddYears( bUp ? 1 : -1 ); +} + +bool DateFormatter::ImplAllowMalformedInput() const +{ + return !IsEnforceValidValue(); +} + +int DateFormatter::GetDateArea(ExtDateFieldFormat& eFormat, std::u16string_view rText, int nCursor, const LocaleDataWrapper& rLocaleDataWrapper) +{ + sal_Int8 nDateArea = 0; + + if ( eFormat == ExtDateFieldFormat::SystemLong ) + { + eFormat = ImplGetExtFormat(rLocaleDataWrapper.getLongDateOrder()); + nDateArea = 1; + } + else + { + // search area + size_t nPos = 0; + OUString aDateSep = ImplGetDateSep(rLocaleDataWrapper, eFormat); + for ( sal_Int8 i = 1; i <= 3; i++ ) + { + nPos = rText.find( aDateSep, nPos ); + if (nPos == std::u16string_view::npos || static_cast<sal_Int32>(nPos) >= nCursor) + { + nDateArea = i; + break; + } + else + nPos++; + } + } + + return nDateArea; +} + +void DateField::ImplDateSpinArea( bool bUp ) +{ + // increment days if all is selected + if ( !GetField() ) + return; + + Date aDate( GetDate() ); + Selection aSelection = GetField()->GetSelection(); + aSelection.Justify(); + OUString aText( GetText() ); + if ( static_cast<sal_Int32>(aSelection.Len()) == aText.getLength() ) + ImplDateIncrementDay( aDate, bUp ); + else + { + ExtDateFieldFormat eFormat = GetExtDateFormat( true ); + sal_Int8 nDateArea = GetDateArea(eFormat, aText, aSelection.Max(), ImplGetLocaleDataWrapper()); + + switch( eFormat ) + { + case ExtDateFieldFormat::ShortMMDDYY: + case ExtDateFieldFormat::ShortMMDDYYYY: + switch( nDateArea ) + { + case 1: ImplDateIncrementMonth( aDate, bUp ); + break; + case 2: ImplDateIncrementDay( aDate, bUp ); + break; + case 3: ImplDateIncrementYear( aDate, bUp ); + break; + } + break; + case ExtDateFieldFormat::ShortDDMMYY: + case ExtDateFieldFormat::ShortDDMMYYYY: + switch( nDateArea ) + { + case 1: ImplDateIncrementDay( aDate, bUp ); + break; + case 2: ImplDateIncrementMonth( aDate, bUp ); + break; + case 3: ImplDateIncrementYear( aDate, bUp ); + break; + } + break; + case ExtDateFieldFormat::ShortYYMMDD: + case ExtDateFieldFormat::ShortYYYYMMDD: + case ExtDateFieldFormat::ShortYYMMDD_DIN5008: + case ExtDateFieldFormat::ShortYYYYMMDD_DIN5008: + switch( nDateArea ) + { + case 1: ImplDateIncrementYear( aDate, bUp ); + break; + case 2: ImplDateIncrementMonth( aDate, bUp ); + break; + case 3: ImplDateIncrementDay( aDate, bUp ); + break; + } + break; + default: + OSL_FAIL( "invalid conversion" ); + break; + } + } + + ImplNewFieldValue( aDate ); +} + +DateFormatter::DateFormatter(Edit* pEdit) + : FormatterBase(pEdit) + , maFieldDate(0) + , maLastDate(0) + , maMin(1, 1, 1900) + , maMax(31, 12, 2200) + , mbLongFormat(false) + , mbShowDateCentury(true) + , mnExtDateFormat(ExtDateFieldFormat::SystemShort) + , mbEnforceValidValue(true) +{ +} + +DateFormatter::~DateFormatter() +{ +} + +CalendarWrapper& DateFormatter::GetCalendarWrapper() const +{ + if (!mxCalendarWrapper) + { + const_cast<DateFormatter*>(this)->mxCalendarWrapper.reset( new CalendarWrapper( comphelper::getProcessComponentContext() ) ); + mxCalendarWrapper->loadDefaultCalendar( GetLocale() ); + } + + return *mxCalendarWrapper; +} + +void DateFormatter::SetExtDateFormat( ExtDateFieldFormat eFormat ) +{ + mnExtDateFormat = eFormat; + ReformatAll(); +} + +ExtDateFieldFormat DateFormatter::GetExtDateFormat( bool bResolveSystemFormat ) const +{ + ExtDateFieldFormat eDateFormat = mnExtDateFormat; + + if (bResolveSystemFormat) + eDateFormat = ResolveSystemFormat(eDateFormat, ImplGetLocaleDataWrapper()); + + return eDateFormat; +} + +void DateFormatter::ReformatAll() +{ + Reformat(); +} + +void DateFormatter::SetMin( const Date& rNewMin ) +{ + maMin = rNewMin; + if ( !IsEmptyFieldValue() ) + ReformatAll(); +} + +void DateFormatter::SetMax( const Date& rNewMax ) +{ + maMax = rNewMax; + if ( !IsEmptyFieldValue() ) + ReformatAll(); +} + +void DateFormatter::SetLongFormat( bool bLong ) +{ + mbLongFormat = bLong; + + // #91913# Remove LongFormat and DateShowCentury - redundant + if ( bLong ) + { + SetExtDateFormat( ExtDateFieldFormat::SystemLong ); + } + else + { + if( mnExtDateFormat == ExtDateFieldFormat::SystemLong ) + SetExtDateFormat( ExtDateFieldFormat::SystemShort ); + } + + ReformatAll(); +} + +namespace +{ + ExtDateFieldFormat ChangeDateCentury(ExtDateFieldFormat eExtDateFormat, bool bShowDateCentury) + { + // #91913# Remove LongFormat and DateShowCentury - redundant + if (bShowDateCentury) + { + switch (eExtDateFormat) + { + case ExtDateFieldFormat::SystemShort: + case ExtDateFieldFormat::SystemShortYY: + eExtDateFormat = ExtDateFieldFormat::SystemShortYYYY; break; + case ExtDateFieldFormat::ShortDDMMYY: + eExtDateFormat = ExtDateFieldFormat::ShortDDMMYYYY; break; + case ExtDateFieldFormat::ShortMMDDYY: + eExtDateFormat = ExtDateFieldFormat::ShortMMDDYYYY; break; + case ExtDateFieldFormat::ShortYYMMDD: + eExtDateFormat = ExtDateFieldFormat::ShortYYYYMMDD; break; + case ExtDateFieldFormat::ShortYYMMDD_DIN5008: + eExtDateFormat = ExtDateFieldFormat::ShortYYYYMMDD_DIN5008; break; + default: + ; + } + } + else + { + switch (eExtDateFormat) + { + case ExtDateFieldFormat::SystemShort: + case ExtDateFieldFormat::SystemShortYYYY: + eExtDateFormat = ExtDateFieldFormat::SystemShortYY; break; + case ExtDateFieldFormat::ShortDDMMYYYY: + eExtDateFormat = ExtDateFieldFormat::ShortDDMMYY; break; + case ExtDateFieldFormat::ShortMMDDYYYY: + eExtDateFormat = ExtDateFieldFormat::ShortMMDDYY; break; + case ExtDateFieldFormat::ShortYYYYMMDD: + eExtDateFormat = ExtDateFieldFormat::ShortYYMMDD; break; + case ExtDateFieldFormat::ShortYYYYMMDD_DIN5008: + eExtDateFormat = ExtDateFieldFormat::ShortYYMMDD_DIN5008; break; + default: + ; + } + } + + return eExtDateFormat; + } +} + +void DateFormatter::SetShowDateCentury( bool bShowDateCentury ) +{ + mbShowDateCentury = bShowDateCentury; + + SetExtDateFormat(ChangeDateCentury(GetExtDateFormat(), bShowDateCentury)); + + ReformatAll(); +} + +void DateFormatter::SetDate( const Date& rNewDate ) +{ + ImplSetUserDate( rNewDate ); + maFieldDate = maLastDate; + maLastDate = GetDate(); +} + +void DateFormatter::ImplSetUserDate( const Date& rNewDate, Selection const * pNewSelection ) +{ + Date aNewDate = rNewDate; + if ( aNewDate > maMax ) + aNewDate = maMax; + else if ( aNewDate < maMin ) + aNewDate = maMin; + maLastDate = aNewDate; + + if ( GetField() ) + ImplSetText( ImplGetDateAsText( aNewDate ), pNewSelection ); +} + +void DateFormatter::ImplNewFieldValue( const Date& rDate ) +{ + if ( !GetField() ) + return; + + Selection aSelection = GetField()->GetSelection(); + aSelection.Justify(); + OUString aText = GetField()->GetText(); + + // If selected until the end then keep it that way + if ( static_cast<sal_Int32>(aSelection.Max()) == aText.getLength() ) + { + if ( !aSelection.Len() ) + aSelection.Min() = SELECTION_MAX; + aSelection.Max() = SELECTION_MAX; + } + + Date aOldLastDate = maLastDate; + ImplSetUserDate( rDate, &aSelection ); + maLastDate = aOldLastDate; + + // Modify at Edit is only set at KeyInput + if ( GetField()->GetText() != aText ) + { + GetField()->SetModifyFlag(); + GetField()->Modify(); + } +} + +Date DateFormatter::GetDate() const +{ + Date aDate( Date::EMPTY ); + + if ( GetField() ) + { + if (TextToDate(GetField()->GetText(), aDate, GetExtDateFormat(true), ImplGetLocaleDataWrapper(), GetCalendarWrapper())) + { + if ( aDate > maMax ) + aDate = maMax; + else if ( aDate < maMin ) + aDate = maMin; + } + else + { + // !!! We should find out why dates are treated differently than other fields (see + // also bug: 52384) + + if ( !ImplAllowMalformedInput() ) + { + if ( maLastDate.GetDate() ) + aDate = maLastDate; + else if ( !IsEmptyFieldValueEnabled() ) + aDate = Date( Date::SYSTEM ); + } + else + aDate = Date( Date::EMPTY ); // set invalid date + } + } + + return aDate; +} + +void DateFormatter::SetEmptyDate() +{ + FormatterBase::SetEmptyFieldValue(); +} + +bool DateFormatter::IsEmptyDate() const +{ + bool bEmpty = FormatterBase::IsEmptyFieldValue(); + + if ( GetField() && MustBeReformatted() && IsEmptyFieldValueEnabled() ) + { + if ( GetField()->GetText().isEmpty() ) + { + bEmpty = true; + } + else if ( !maLastDate.GetDate() ) + { + Date aDate( Date::EMPTY ); + bEmpty = !TextToDate(GetField()->GetText(), aDate, GetExtDateFormat(true), ImplGetLocaleDataWrapper(), GetCalendarWrapper()); + } + } + return bEmpty; +} + +void DateFormatter::Reformat() +{ + if ( !GetField() ) + return; + + if ( GetField()->GetText().isEmpty() && ImplGetEmptyFieldValue() ) + return; + + OUString aStr; + ImplDateReformat( GetField()->GetText(), aStr ); + + if ( !aStr.isEmpty() ) + { + ImplSetText( aStr ); + (void)TextToDate(aStr, maLastDate, GetExtDateFormat(true), ImplGetLocaleDataWrapper(), GetCalendarWrapper()); + } + else + { + if ( maLastDate.GetDate() ) + SetDate( maLastDate ); + else if ( !IsEmptyFieldValueEnabled() ) + SetDate( Date( Date::SYSTEM ) ); + else + { + ImplSetText( OUString() ); + SetEmptyFieldValueData( true ); + } + } +} + +void DateFormatter::ExpandCentury( Date& rDate ) +{ + ExpandCentury(rDate, officecfg::Office::Common::DateFormat::TwoDigitYear::get()); +} + +void DateFormatter::ExpandCentury( Date& rDate, sal_uInt16 nTwoDigitYearStart ) +{ + sal_Int16 nDateYear = rDate.GetYear(); + if ( 0 <= nDateYear && nDateYear < 100 ) + { + sal_uInt16 nCentury = nTwoDigitYearStart / 100; + if ( nDateYear < (nTwoDigitYearStart % 100) ) + nCentury++; + rDate.SetYear( nDateYear + (nCentury*100) ); + } +} + +DateField::DateField( vcl::Window* pParent, WinBits nWinStyle ) : + SpinField( pParent, nWinStyle ), + DateFormatter(this), + maFirst( GetMin() ), + maLast( GetMax() ) +{ + SetText( ImplGetLocaleDataWrapper().getDate( ImplGetFieldDate() ) ); + Reformat(); + ResetLastDate(); +} + +void DateField::dispose() +{ + ClearField(); + SpinField::dispose(); +} + +bool DateField::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && IsStrictFormat() && + ( GetExtDateFormat() != ExtDateFieldFormat::SystemLong ) && + !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplDateProcessKeyInput( *rNEvt.GetKeyEvent(), GetExtDateFormat( true ), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return SpinField::PreNotify( rNEvt ); +} + +bool DateField::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() ) + { + // !!! We should find out why dates are treated differently than other fields (see + // also bug: 52384) + + bool bTextLen = !GetText().isEmpty(); + if ( bTextLen || !IsEmptyFieldValueEnabled() ) + { + if ( !ImplAllowMalformedInput() ) + Reformat(); + else + { + Date aDate( 0, 0, 0 ); + if (TextToDate(GetText(), aDate, GetExtDateFormat(true), ImplGetLocaleDataWrapper(), GetCalendarWrapper())) + // even with strict text analysis, our text is a valid date -> do a complete + // reformat + Reformat(); + } + } + else + { + ResetLastDate(); + SetEmptyFieldValueData( true ); + } + } + } + + return SpinField::EventNotify( rNEvt ); +} + +void DateField::DataChanged( const DataChangedEvent& rDCEvt ) +{ + SpinField::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & (AllSettingsFlags::LOCALE|AllSettingsFlags::MISC)) ) + { + if (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) + ImplResetLocaleDataWrapper(); + ReformatAll(); + } +} + +void DateField::Modify() +{ + MarkToBeReformatted( true ); + SpinField::Modify(); +} + +void DateField::Up() +{ + ImplDateSpinArea( true ); + SpinField::Up(); +} + +void DateField::Down() +{ + ImplDateSpinArea( false ); + SpinField::Down(); +} + +void DateField::First() +{ + ImplNewFieldValue( maFirst ); + SpinField::First(); +} + +void DateField::Last() +{ + ImplNewFieldValue( maLast ); + SpinField::Last(); +} + +DateBox::DateBox(vcl::Window* pParent, WinBits nWinStyle) + : ComboBox( pParent, nWinStyle ) + , DateFormatter(this) +{ + SetText( ImplGetLocaleDataWrapper().getDate( ImplGetFieldDate() ) ); + Reformat(); +} + +void DateBox::dispose() +{ + ClearField(); + ComboBox::dispose(); +} + +bool DateBox::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && IsStrictFormat() && + ( GetExtDateFormat() != ExtDateFieldFormat::SystemLong ) && + !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplDateProcessKeyInput( *rNEvt.GetKeyEvent(), GetExtDateFormat( true ), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return ComboBox::PreNotify( rNEvt ); +} + +void DateBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + ComboBox::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + ImplResetLocaleDataWrapper(); + ReformatAll(); + } +} + +bool DateBox::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() ) + { + bool bTextLen = !GetText().isEmpty(); + if ( bTextLen || !IsEmptyFieldValueEnabled() ) + Reformat(); + else + { + ResetLastDate(); + SetEmptyFieldValueData( true ); + } + } + } + + return ComboBox::EventNotify( rNEvt ); +} + +void DateBox::Modify() +{ + MarkToBeReformatted( true ); + ComboBox::Modify(); +} + +void DateBox::ReformatAll() +{ + OUString aStr; + SetUpdateMode( false ); + const sal_Int32 nEntryCount = GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; ++i ) + { + ImplDateReformat( GetEntry( i ), aStr ); + RemoveEntryAt(i); + InsertEntry( aStr, i ); + } + DateFormatter::Reformat(); + SetUpdateMode( true ); +} + +namespace weld +{ + CalendarWrapper& DateFormatter::GetCalendarWrapper() const + { + if (!m_xCalendarWrapper) + { + m_xCalendarWrapper.reset(new CalendarWrapper(comphelper::getProcessComponentContext())); + m_xCalendarWrapper->loadDefaultCalendar(Application::GetSettings().GetLanguageTag().getLocale()); + } + return *m_xCalendarWrapper; + } + + void DateFormatter::SetShowDateCentury(bool bShowDateCentury) + { + m_eFormat = ChangeDateCentury(m_eFormat, bShowDateCentury); + + ReFormat(); + } + + void DateFormatter::SetDate(const Date& rDate) + { + auto nDate = rDate.GetDate(); + bool bForceOutput = GetEntryText().isEmpty() && rDate == GetDate(); + if (bForceOutput) + { + ImplSetValue(nDate, true); + return; + } + SetValue(nDate); + } + + Date DateFormatter::GetDate() + { + return Date(GetValue()); + } + + void DateFormatter::SetMin(const Date& rNewMin) + { + SetMinValue(rNewMin.GetDate()); + } + + void DateFormatter::SetMax(const Date& rNewMax) + { + SetMaxValue(rNewMax.GetDate()); + } + + OUString DateFormatter::FormatNumber(int nValue) const + { + const LocaleDataWrapper& rLocaleData = Application::GetSettings().GetLocaleDataWrapper(); + return ::DateFormatter::FormatDate(Date(nValue), m_eFormat, rLocaleData, m_aStaticFormatter); + } + + IMPL_LINK_NOARG(DateFormatter, FormatOutputHdl, LinkParamNone*, bool) + { + OUString sText = FormatNumber(GetValue()); + ImplSetTextImpl(sText, nullptr); + return true; + } + + IMPL_LINK(DateFormatter, ParseInputHdl, sal_Int64*, result, TriState) + { + const LocaleDataWrapper& rLocaleDataWrapper = Application::GetSettings().GetLocaleDataWrapper(); + + Date aResult(Date::EMPTY); + bool bRet = ::DateFormatter::TextToDate(GetEntryText(), aResult, ResolveSystemFormat(m_eFormat, rLocaleDataWrapper), + rLocaleDataWrapper, GetCalendarWrapper()); + if (bRet) + *result = aResult.GetDate(); + + return bRet ? TRISTATE_TRUE : TRISTATE_FALSE; + } +} + +static bool ImplTimeProcessKeyInput( const KeyEvent& rKEvt, + bool bStrictFormat, bool bDuration, + TimeFieldFormat eFormat, + const LocaleDataWrapper& rLocaleDataWrapper ) +{ + sal_Unicode cChar = rKEvt.GetCharCode(); + + if ( !bStrictFormat ) + return false; + else + { + sal_uInt16 nGroup = rKEvt.GetKeyCode().GetGroup(); + if ( (nGroup == KEYGROUP_FKEYS) || (nGroup == KEYGROUP_CURSOR) || + (nGroup == KEYGROUP_MISC) || + ((cChar >= '0') && (cChar <= '9')) || + rLocaleDataWrapper.getTimeSep() == OUStringChar(cChar) || + (rLocaleDataWrapper.getTimeAM().indexOf(cChar) != -1) || + (rLocaleDataWrapper.getTimePM().indexOf(cChar) != -1) || + // Accept AM/PM: + (cChar == 'a') || (cChar == 'A') || (cChar == 'm') || (cChar == 'M') || (cChar == 'p') || (cChar == 'P') || + ((eFormat == TimeFieldFormat::F_SEC_CS) && rLocaleDataWrapper.getTime100SecSep() == OUStringChar(cChar)) || + (bDuration && (cChar == '-')) ) + return false; + else + return true; + } +} + +static bool ImplIsOnlyDigits( const OUString& _rStr ) +{ + const sal_Unicode* _pChr = _rStr.getStr(); + for ( sal_Int32 i = 0; i < _rStr.getLength(); ++i, ++_pChr ) + { + if ( *_pChr < '0' || *_pChr > '9' ) + return false; + } + return true; +} + +static bool ImplIsValidTimePortion( bool _bSkipInvalidCharacters, const OUString& _rStr ) +{ + if ( !_bSkipInvalidCharacters ) + { + if ( ( _rStr.getLength() > 2 ) || _rStr.isEmpty() || !ImplIsOnlyDigits( _rStr ) ) + return false; + } + return true; +} + +static bool ImplCutTimePortion( OUStringBuffer& _rStr, sal_Int32 _nSepPos, bool _bSkipInvalidCharacters, short* _pPortion ) +{ + OUString sPortion(_rStr.getStr(), _nSepPos ); + + if (_nSepPos < _rStr.getLength()) + _rStr.remove(0, _nSepPos + 1); + else + _rStr.truncate(); + + if ( !ImplIsValidTimePortion( _bSkipInvalidCharacters, sPortion ) ) + return false; + *_pPortion = static_cast<short>(sPortion.toInt32()); + return true; +} + +bool TimeFormatter::TextToTime(std::u16string_view rStr, tools::Time& rTime, + TimeFieldFormat eFormat, + bool bDuration, const LocaleDataWrapper& rLocaleDataWrapper, bool _bSkipInvalidCharacters) +{ + OUStringBuffer aStr(rStr); + short nHour = 0; + short nMinute = 0; + short nSecond = 0; + sal_Int64 nNanoSec = 0; + tools::Time aTime( 0, 0, 0 ); + + if ( rStr.empty() ) + return false; + + // Search for separators + if (!rLocaleDataWrapper.getTimeSep().isEmpty()) + { + OUStringBuffer aSepStr(",.;:/"); + if ( !bDuration ) + aSepStr.append('-'); + + // Replace characters above by the separator character + for (sal_Int32 i = 0; i < aSepStr.getLength(); ++i) + { + if (rLocaleDataWrapper.getTimeSep() == OUStringChar(aSepStr[i])) + continue; + for ( sal_Int32 j = 0; j < aStr.getLength(); j++ ) + { + if (aStr[j] == aSepStr[i]) + aStr[j] = rLocaleDataWrapper.getTimeSep()[0]; + } + } + } + + bool bNegative = false; + sal_Int32 nSepPos = aStr.indexOf( rLocaleDataWrapper.getTimeSep() ); + if ( aStr[0] == '-' ) + bNegative = true; + if ( eFormat != TimeFieldFormat::F_SEC_CS ) + { + if ( nSepPos < 0 ) + nSepPos = aStr.getLength(); + if ( !ImplCutTimePortion( aStr, nSepPos, _bSkipInvalidCharacters, &nHour ) ) + return false; + + nSepPos = aStr.indexOf( rLocaleDataWrapper.getTimeSep() ); + if ( !aStr.isEmpty() && aStr[0] == '-' ) + bNegative = true; + if ( nSepPos >= 0 ) + { + if ( !ImplCutTimePortion( aStr, nSepPos, _bSkipInvalidCharacters, &nMinute ) ) + return false; + + nSepPos = aStr.indexOf( rLocaleDataWrapper.getTimeSep() ); + if ( !aStr.isEmpty() && aStr[0] == '-' ) + bNegative = true; + if ( nSepPos >= 0 ) + { + if ( !ImplCutTimePortion( aStr, nSepPos, _bSkipInvalidCharacters, &nSecond ) ) + return false; + if ( !aStr.isEmpty() && aStr[0] == '-' ) + bNegative = true; + nNanoSec = o3tl::toInt64(aStr); + } + else + nSecond = static_cast<short>(o3tl::toInt32(aStr)); + } + else + nMinute = static_cast<short>(o3tl::toInt32(aStr)); + } + else if ( nSepPos < 0 ) + { + nSecond = static_cast<short>(o3tl::toInt32(aStr)); + nMinute += nSecond / 60; + nSecond %= 60; + nHour += nMinute / 60; + nMinute %= 60; + } + else + { + nSecond = static_cast<short>(o3tl::toInt32(aStr.subView( 0, nSepPos ))); + aStr.remove( 0, nSepPos+1 ); + + nSepPos = aStr.indexOf( rLocaleDataWrapper.getTimeSep() ); + if ( !aStr.isEmpty() && aStr[0] == '-' ) + bNegative = true; + if ( nSepPos >= 0 ) + { + nMinute = nSecond; + nSecond = static_cast<short>(o3tl::toInt32(aStr.subView( 0, nSepPos ))); + aStr.remove( 0, nSepPos+1 ); + + nSepPos = aStr.indexOf( rLocaleDataWrapper.getTimeSep() ); + if ( !aStr.isEmpty() && aStr[0] == '-' ) + bNegative = true; + if ( nSepPos >= 0 ) + { + nHour = nMinute; + nMinute = nSecond; + nSecond = static_cast<short>(o3tl::toInt32(aStr.subView( 0, nSepPos ))); + aStr.remove( 0, nSepPos+1 ); + } + else + { + nHour += nMinute / 60; + nMinute %= 60; + } + } + else + { + nMinute += nSecond / 60; + nSecond %= 60; + nHour += nMinute / 60; + nMinute %= 60; + } + nNanoSec = o3tl::toInt64(aStr); + } + + if ( nNanoSec ) + { + assert(aStr.getLength() >= 1); + + sal_Int32 nLen = 1; // at least one digit, otherwise nNanoSec==0 + + while ( aStr.getLength() > nLen && aStr[nLen] >= '0' && aStr[nLen] <= '9' ) + nLen++; + + while ( nLen < 9) + { + nNanoSec *= 10; + ++nLen; + } + while ( nLen > 9 ) + { + // round if negative? + nNanoSec = (nNanoSec + 5) / 10; + --nLen; + } + } + + assert(nNanoSec > -1000000000 && nNanoSec < 1000000000); + if ( (nMinute > 59) || (nSecond > 59) || (nNanoSec > 1000000000) ) + return false; + + if ( eFormat == TimeFieldFormat::F_NONE ) + nSecond = nNanoSec = 0; + else if ( eFormat == TimeFieldFormat::F_SEC ) + nNanoSec = 0; + + if ( !bDuration ) + { + if ( bNegative || (nHour < 0) || (nMinute < 0) || + (nSecond < 0) || (nNanoSec < 0) ) + return false; + + OUString aUpperCaseStr = aStr.toString().toAsciiUpperCase(); + OUString aAMlocalised(rLocaleDataWrapper.getTimeAM().toAsciiUpperCase()); + OUString aPMlocalised(rLocaleDataWrapper.getTimePM().toAsciiUpperCase()); + + if ( (nHour < 12) && ( ( aUpperCaseStr.indexOf( "PM" ) >= 0 ) || ( aUpperCaseStr.indexOf( aPMlocalised ) >= 0 ) ) ) + nHour += 12; + + if ( (nHour == 12) && ( ( aUpperCaseStr.indexOf( "AM" ) >= 0 ) || ( aUpperCaseStr.indexOf( aAMlocalised ) >= 0 ) ) ) + nHour = 0; + + aTime = tools::Time( static_cast<sal_uInt16>(nHour), static_cast<sal_uInt16>(nMinute), static_cast<sal_uInt16>(nSecond), + static_cast<sal_uInt32>(nNanoSec) ); + } + else + { + assert( !bNegative || (nHour < 0) || (nMinute < 0) || + (nSecond < 0) || (nNanoSec < 0) ); + if ( bNegative || (nHour < 0) || (nMinute < 0) || + (nSecond < 0) || (nNanoSec < 0) ) + { + // LEM TODO: this looks weird... I think buggy when parsing "05:-02:18" + bNegative = true; + nHour = nHour < 0 ? -nHour : nHour; + nMinute = nMinute < 0 ? -nMinute : nMinute; + nSecond = nSecond < 0 ? -nSecond : nSecond; + nNanoSec = nNanoSec < 0 ? -nNanoSec : nNanoSec; + } + + aTime = tools::Time( static_cast<sal_uInt16>(nHour), static_cast<sal_uInt16>(nMinute), static_cast<sal_uInt16>(nSecond), + static_cast<sal_uInt32>(nNanoSec) ); + if ( bNegative ) + aTime = -aTime; + } + + rTime = aTime; + + return true; +} + +void TimeFormatter::ImplTimeReformat( std::u16string_view rStr, OUString& rOutStr ) +{ + tools::Time aTime( 0, 0, 0 ); + if ( !TextToTime( rStr, aTime, GetFormat(), IsDuration(), ImplGetLocaleDataWrapper() ) ) + return; + + tools::Time aTempTime = aTime; + if ( aTempTime > GetMax() ) + aTempTime = GetMax() ; + else if ( aTempTime < GetMin() ) + aTempTime = GetMin(); + + bool bSecond = false; + bool b100Sec = false; + if ( meFormat != TimeFieldFormat::F_NONE ) + bSecond = true; + + if ( meFormat == TimeFieldFormat::F_SEC_CS ) + { + sal_uLong n = aTempTime.GetHour() * 3600L; + n += aTempTime.GetMin() * 60L; + n += aTempTime.GetSec(); + rOutStr = OUString::number( n ); + rOutStr += ImplGetLocaleDataWrapper().getTime100SecSep(); + std::ostringstream ostr; + ostr.fill('0'); + ostr.width(9); + ostr << aTempTime.GetNanoSec(); + rOutStr += OUString::createFromAscii(ostr.str().c_str()); + } + else if ( mbDuration ) + rOutStr = ImplGetLocaleDataWrapper().getDuration( aTempTime, bSecond, b100Sec ); + else + { + rOutStr = ImplGetLocaleDataWrapper().getTime( aTempTime, bSecond, b100Sec ); + if ( GetTimeFormat() == TimeFormat::Hour12 ) + { + if ( aTempTime.GetHour() > 12 ) + { + tools::Time aT( aTempTime ); + aT.SetHour( aT.GetHour() % 12 ); + rOutStr = ImplGetLocaleDataWrapper().getTime( aT, bSecond, b100Sec ); + } + // Don't use LocaleDataWrapper, we want AM/PM + if ( aTempTime.GetHour() < 12 ) + rOutStr += "AM"; // ImplGetLocaleDataWrapper().getTimeAM(); + else + rOutStr += "PM"; // ImplGetLocaleDataWrapper().getTimePM(); + } + } +} + +bool TimeFormatter::ImplAllowMalformedInput() const +{ + return !IsEnforceValidValue(); +} + +int TimeFormatter::GetTimeArea(TimeFieldFormat eFormat, std::u16string_view rText, int nCursor, + const LocaleDataWrapper& rLocaleDataWrapper) +{ + int nTimeArea = 0; + + // Area search + if (eFormat != TimeFieldFormat::F_SEC_CS) + { + //Which area is the cursor in of HH:MM:SS.TT + for ( size_t i = 1, nPos = 0; i <= 4; i++ ) + { + size_t nPos1 = rText.find(rLocaleDataWrapper.getTimeSep(), nPos); + size_t nPos2 = rText.find(rLocaleDataWrapper.getTime100SecSep(), nPos); + //which ever comes first, bearing in mind that one might not be there + if (nPos1 != std::u16string_view::npos && nPos2 != std::u16string_view::npos) + nPos = std::min(nPos1, nPos2); + else if (nPos1 != std::u16string_view::npos) + nPos = nPos1; + else + nPos = nPos2; + if (nPos == std::u16string_view::npos || static_cast<sal_Int32>(nPos) >= nCursor) + { + nTimeArea = i; + break; + } + else + nPos++; + } + } + else + { + size_t nPos = rText.find(rLocaleDataWrapper.getTime100SecSep()); + if (nPos == std::u16string_view::npos || static_cast<sal_Int32>(nPos) >= nCursor) + nTimeArea = 3; + else + nTimeArea = 4; + } + + return nTimeArea; +} + +tools::Time TimeFormatter::SpinTime(bool bUp, const tools::Time& rTime, TimeFieldFormat eFormat, + bool bDuration, std::u16string_view rText, int nCursor, + const LocaleDataWrapper& rLocaleDataWrapper) +{ + tools::Time aTime(rTime); + + int nTimeArea = GetTimeArea(eFormat, rText, nCursor, rLocaleDataWrapper); + + if ( nTimeArea ) + { + tools::Time aAddTime( 0, 0, 0 ); + if ( nTimeArea == 1 ) + aAddTime = tools::Time( 1, 0 ); + else if ( nTimeArea == 2 ) + aAddTime = tools::Time( 0, 1 ); + else if ( nTimeArea == 3 ) + aAddTime = tools::Time( 0, 0, 1 ); + else if ( nTimeArea == 4 ) + aAddTime = tools::Time( 0, 0, 0, 1 ); + + if ( !bUp ) + aAddTime = -aAddTime; + + aTime += aAddTime; + if (!bDuration) + { + tools::Time aAbsMaxTime( 23, 59, 59, 999999999 ); + if ( aTime > aAbsMaxTime ) + aTime = aAbsMaxTime; + tools::Time aAbsMinTime( 0, 0 ); + if ( aTime < aAbsMinTime ) + aTime = aAbsMinTime; + } + } + + return aTime; +} + +void TimeField::ImplTimeSpinArea( bool bUp ) +{ + if ( GetField() ) + { + tools::Time aTime( GetTime() ); + OUString aText( GetText() ); + Selection aSelection( GetField()->GetSelection() ); + + aTime = TimeFormatter::SpinTime(bUp, aTime, GetFormat(), IsDuration(), aText, aSelection.Max(), ImplGetLocaleDataWrapper()); + + ImplNewFieldValue( aTime ); + } +} + +TimeFormatter::TimeFormatter(Edit* pEdit) + : FormatterBase(pEdit) + , maLastTime(0, 0) + , maMin(0, 0) + , maMax(23, 59, 59, 999999999) + , meFormat(TimeFieldFormat::F_NONE) + , mnTimeFormat(TimeFormat::Hour24) // Should become an ExtTimeFieldFormat in next implementation, merge with mbDuration and meFormat + , mbDuration(false) + , mbEnforceValidValue(true) + , maFieldTime(0, 0) +{ +} + +TimeFormatter::~TimeFormatter() +{ +} + +void TimeFormatter::ReformatAll() +{ + Reformat(); +} + +void TimeFormatter::SetMin( const tools::Time& rNewMin ) +{ + maMin = rNewMin; + if ( !IsEmptyFieldValue() ) + ReformatAll(); +} + +void TimeFormatter::SetMax( const tools::Time& rNewMax ) +{ + maMax = rNewMax; + if ( !IsEmptyFieldValue() ) + ReformatAll(); +} + +void TimeFormatter::SetTimeFormat( TimeFormat eNewFormat ) +{ + mnTimeFormat = eNewFormat; +} + + +void TimeFormatter::SetFormat( TimeFieldFormat eNewFormat ) +{ + meFormat = eNewFormat; + ReformatAll(); +} + +void TimeFormatter::SetDuration( bool bNewDuration ) +{ + mbDuration = bNewDuration; + ReformatAll(); +} + +void TimeFormatter::SetTime( const tools::Time& rNewTime ) +{ + SetUserTime( rNewTime ); + maFieldTime = maLastTime; + SetEmptyFieldValueData( false ); +} + +void TimeFormatter::ImplNewFieldValue( const tools::Time& rTime ) +{ + if ( !GetField() ) + return; + + Selection aSelection = GetField()->GetSelection(); + aSelection.Justify(); + OUString aText = GetField()->GetText(); + + // If selected until the end then keep it that way + if ( static_cast<sal_Int32>(aSelection.Max()) == aText.getLength() ) + { + if ( !aSelection.Len() ) + aSelection.Min() = SELECTION_MAX; + aSelection.Max() = SELECTION_MAX; + } + + tools::Time aOldLastTime = maLastTime; + ImplSetUserTime( rTime, &aSelection ); + maLastTime = aOldLastTime; + + // Modify at Edit is only set at KeyInput + if ( GetField()->GetText() != aText ) + { + GetField()->SetModifyFlag(); + GetField()->Modify(); + } +} + +OUString TimeFormatter::FormatTime(const tools::Time& rNewTime, TimeFieldFormat eFormat, TimeFormat eHourFormat, bool bDuration, const LocaleDataWrapper& rLocaleData) +{ + OUString aStr; + bool bSec = false; + bool b100Sec = false; + if ( eFormat != TimeFieldFormat::F_NONE ) + bSec = true; + if ( eFormat == TimeFieldFormat::F_SEC_CS ) + b100Sec = true; + if ( eFormat == TimeFieldFormat::F_SEC_CS ) + { + sal_uLong n = rNewTime.GetHour() * 3600L; + n += rNewTime.GetMin() * 60L; + n += rNewTime.GetSec(); + aStr = OUString::number( n ) + rLocaleData.getTime100SecSep(); + std::ostringstream ostr; + ostr.fill('0'); + ostr.width(9); + ostr << rNewTime.GetNanoSec(); + aStr += OUString::createFromAscii(ostr.str().c_str()); + } + else if ( bDuration ) + { + aStr = rLocaleData.getDuration( rNewTime, bSec, b100Sec ); + } + else + { + aStr = rLocaleData.getTime( rNewTime, bSec, b100Sec ); + if ( eHourFormat == TimeFormat::Hour12 ) + { + if ( rNewTime.GetHour() > 12 ) + { + tools::Time aT( rNewTime ); + aT.SetHour( aT.GetHour() % 12 ); + aStr = rLocaleData.getTime( aT, bSec, b100Sec ); + } + // Don't use LocaleDataWrapper, we want AM/PM + if ( rNewTime.GetHour() < 12 ) + aStr += "AM"; // rLocaleData.getTimeAM(); + else + aStr += "PM"; // rLocaleData.getTimePM(); + } + } + + return aStr; +} + +void TimeFormatter::ImplSetUserTime( const tools::Time& rNewTime, Selection const * pNewSelection ) +{ + tools::Time aNewTime = rNewTime; + if ( aNewTime > GetMax() ) + aNewTime = GetMax(); + else if ( aNewTime < GetMin() ) + aNewTime = GetMin(); + maLastTime = aNewTime; + + if ( GetField() ) + { + OUString aStr = TimeFormatter::FormatTime(aNewTime, meFormat, GetTimeFormat(), mbDuration, ImplGetLocaleDataWrapper()); + ImplSetText( aStr, pNewSelection ); + } +} + +void TimeFormatter::SetUserTime( const tools::Time& rNewTime ) +{ + ImplSetUserTime( rNewTime ); +} + +tools::Time TimeFormatter::GetTime() const +{ + tools::Time aTime( 0, 0, 0 ); + + if ( GetField() ) + { + bool bAllowMalformed = ImplAllowMalformedInput(); + if ( TextToTime( GetField()->GetText(), aTime, GetFormat(), IsDuration(), ImplGetLocaleDataWrapper(), !bAllowMalformed ) ) + { + if ( aTime > GetMax() ) + aTime = GetMax(); + else if ( aTime < GetMin() ) + aTime = GetMin(); + } + else + { + if ( bAllowMalformed ) + aTime = tools::Time( 99, 99, 99 ); // set invalid time + else + aTime = maLastTime; + } + } + + return aTime; +} + +void TimeFormatter::Reformat() +{ + if ( !GetField() ) + return; + + if ( GetField()->GetText().isEmpty() && ImplGetEmptyFieldValue() ) + return; + + OUString aStr; + ImplTimeReformat( GetField()->GetText(), aStr ); + + if ( !aStr.isEmpty() ) + { + ImplSetText( aStr ); + (void)TextToTime(aStr, maLastTime, GetFormat(), IsDuration(), ImplGetLocaleDataWrapper()); + } + else + SetTime( maLastTime ); +} + +TimeField::TimeField( vcl::Window* pParent, WinBits nWinStyle ) : + SpinField( pParent, nWinStyle ), + TimeFormatter(this), + maFirst( GetMin() ), + maLast( GetMax() ) +{ + SetText( ImplGetLocaleDataWrapper().getTime( maFieldTime, false ) ); + Reformat(); +} + +void TimeField::dispose() +{ + ClearField(); + SpinField::dispose(); +} + +bool TimeField::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplTimeProcessKeyInput( *rNEvt.GetKeyEvent(), IsStrictFormat(), IsDuration(), GetFormat(), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return SpinField::PreNotify( rNEvt ); +} + +bool TimeField::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + { + if ( !ImplAllowMalformedInput() ) + Reformat(); + else + { + tools::Time aTime( 0, 0, 0 ); + if ( TextToTime( GetText(), aTime, GetFormat(), IsDuration(), ImplGetLocaleDataWrapper(), false ) ) + // even with strict text analysis, our text is a valid time -> do a complete + // reformat + Reformat(); + } + } + } + + return SpinField::EventNotify( rNEvt ); +} + +void TimeField::DataChanged( const DataChangedEvent& rDCEvt ) +{ + SpinField::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + ImplResetLocaleDataWrapper(); + ReformatAll(); + } +} + +void TimeField::Modify() +{ + MarkToBeReformatted( true ); + SpinField::Modify(); +} + +void TimeField::Up() +{ + ImplTimeSpinArea( true ); + SpinField::Up(); +} + +void TimeField::Down() +{ + ImplTimeSpinArea( false ); + SpinField::Down(); +} + +void TimeField::First() +{ + ImplNewFieldValue( maFirst ); + SpinField::First(); +} + +void TimeField::Last() +{ + ImplNewFieldValue( maLast ); + SpinField::Last(); +} + +void TimeField::SetExtFormat( ExtTimeFieldFormat eFormat ) +{ + switch ( eFormat ) + { + case ExtTimeFieldFormat::Short24H: + { + SetTimeFormat( TimeFormat::Hour24 ); + SetDuration( false ); + SetFormat( TimeFieldFormat::F_NONE ); + } + break; + case ExtTimeFieldFormat::Long24H: + { + SetTimeFormat( TimeFormat::Hour24 ); + SetDuration( false ); + SetFormat( TimeFieldFormat::F_SEC ); + } + break; + case ExtTimeFieldFormat::Short12H: + { + SetTimeFormat( TimeFormat::Hour12 ); + SetDuration( false ); + SetFormat( TimeFieldFormat::F_NONE ); + } + break; + case ExtTimeFieldFormat::Long12H: + { + SetTimeFormat( TimeFormat::Hour12 ); + SetDuration( false ); + SetFormat( TimeFieldFormat::F_SEC ); + } + break; + case ExtTimeFieldFormat::ShortDuration: + { + SetDuration( true ); + SetFormat( TimeFieldFormat::F_NONE ); + } + break; + case ExtTimeFieldFormat::LongDuration: + { + SetDuration( true ); + SetFormat( TimeFieldFormat::F_SEC ); + } + break; + default: OSL_FAIL( "ExtTimeFieldFormat unknown!" ); + } + + if ( GetField() && !GetField()->GetText().isEmpty() ) + SetUserTime( GetTime() ); + ReformatAll(); +} + +TimeBox::TimeBox(vcl::Window* pParent, WinBits nWinStyle) + : ComboBox(pParent, nWinStyle) + , TimeFormatter(this) +{ + SetText( ImplGetLocaleDataWrapper().getTime( maFieldTime, false ) ); + Reformat(); +} + +void TimeBox::dispose() +{ + ClearField(); + ComboBox::dispose(); +} + +bool TimeBox::PreNotify( NotifyEvent& rNEvt ) +{ + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !rNEvt.GetKeyEvent()->GetKeyCode().IsMod2() ) + { + if ( ImplTimeProcessKeyInput( *rNEvt.GetKeyEvent(), IsStrictFormat(), IsDuration(), GetFormat(), ImplGetLocaleDataWrapper() ) ) + return true; + } + + return ComboBox::PreNotify( rNEvt ); +} + +bool TimeBox::EventNotify( NotifyEvent& rNEvt ) +{ + if ( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + MarkToBeReformatted( false ); + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() && (!GetText().isEmpty() || !IsEmptyFieldValueEnabled()) ) + Reformat(); + } + + return ComboBox::EventNotify( rNEvt ); +} + +void TimeBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + ComboBox::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && (rDCEvt.GetFlags() & AllSettingsFlags::LOCALE) ) + { + ImplResetLocaleDataWrapper(); + ReformatAll(); + } +} + +void TimeBox::Modify() +{ + MarkToBeReformatted( true ); + ComboBox::Modify(); +} + +void TimeBox::ReformatAll() +{ + OUString aStr; + SetUpdateMode( false ); + const sal_Int32 nEntryCount = GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; ++i ) + { + ImplTimeReformat( GetEntry( i ), aStr ); + RemoveEntryAt(i); + InsertEntry( aStr, i ); + } + TimeFormatter::Reformat(); + SetUpdateMode( true ); +} + +namespace weld +{ + tools::Time TimeFormatter::ConvertValue(int nValue) + { + tools::Time aTime(0); + aTime.MakeTimeFromMS(nValue); + return aTime; + } + + int TimeFormatter::ConvertValue(const tools::Time& rTime) + { + return rTime.GetMSFromTime(); + } + + void TimeFormatter::SetTime(const tools::Time& rTime) + { + auto nTime = ConvertValue(rTime); + bool bForceOutput = GetEntryText().isEmpty() && rTime == GetTime(); + if (bForceOutput) + { + ImplSetValue(nTime, true); + return; + } + SetValue(nTime); + } + + tools::Time TimeFormatter::GetTime() + { + return ConvertValue(GetValue()); + } + + void TimeFormatter::SetMin(const tools::Time& rNewMin) + { + SetMinValue(ConvertValue(rNewMin)); + } + + void TimeFormatter::SetMax(const tools::Time& rNewMax) + { + SetMaxValue(ConvertValue(rNewMax)); + } + + OUString TimeFormatter::FormatNumber(int nValue) const + { + const LocaleDataWrapper& rLocaleData = Application::GetSettings().GetLocaleDataWrapper(); + return ::TimeFormatter::FormatTime(ConvertValue(nValue), m_eFormat, m_eTimeFormat, m_bDuration, rLocaleData); + } + + IMPL_LINK_NOARG(TimeFormatter, FormatOutputHdl, LinkParamNone*, bool) + { + OUString sText = FormatNumber(GetValue()); + ImplSetTextImpl(sText, nullptr); + return true; + } + + IMPL_LINK(TimeFormatter, ParseInputHdl, sal_Int64*, result, TriState) + { + const LocaleDataWrapper& rLocaleDataWrapper = Application::GetSettings().GetLocaleDataWrapper(); + + tools::Time aResult(0); + bool bRet = ::TimeFormatter::TextToTime(GetEntryText(), aResult, m_eFormat, m_bDuration, rLocaleDataWrapper); + if (bRet) + *result = ConvertValue(aResult); + + return bRet ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + IMPL_LINK(TimeFormatter, CursorChangedHdl, weld::Entry&, rEntry, void) + { + int nStartPos, nEndPos; + rEntry.get_selection_bounds(nStartPos, nEndPos); + + const LocaleDataWrapper& rLocaleData = Application::GetSettings().GetLocaleDataWrapper(); + const int nTimeArea = ::TimeFormatter::GetTimeArea(m_eFormat, GetEntryText(), nEndPos, rLocaleData); + + int nIncrements = 1; + + if (nTimeArea == 1) + nIncrements = 1000 * 60 * 60; + else if (nTimeArea == 2) + nIncrements = 1000 * 60; + else if (nTimeArea == 3) + nIncrements = 1000; + + SetSpinSize(nIncrements); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/fixed.cxx b/vcl/source/control/fixed.cxx new file mode 100644 index 000000000..5b7038bbe --- /dev/null +++ b/vcl/source/control/fixed.cxx @@ -0,0 +1,995 @@ +/* -*- 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 <vcl/cvtgrf.hxx> +#include <vcl/decoview.hxx> +#include <vcl/event.hxx> +#include <vcl/toolkit/fixed.hxx> +#include <vcl/settings.hxx> + +#include <comphelper/base64.hxx> +#include <comphelper/string.hxx> +#include <sal/log.hxx> +#include <tools/json_writer.hxx> +#include <tools/stream.hxx> + +#define FIXEDLINE_TEXT_BORDER 4 + +constexpr auto FIXEDTEXT_VIEW_STYLE = WB_3DLOOK | + WB_LEFT | WB_CENTER | WB_RIGHT | + WB_TOP | WB_VCENTER | WB_BOTTOM | + WB_WORDBREAK | WB_NOLABEL | + WB_PATHELLIPSIS; +constexpr auto FIXEDLINE_VIEW_STYLE = WB_3DLOOK | WB_NOLABEL; +constexpr auto FIXEDBITMAP_VIEW_STYLE = WB_3DLOOK | + WB_LEFT | WB_CENTER | WB_RIGHT | + WB_TOP | WB_VCENTER | WB_BOTTOM | + WB_SCALE; +constexpr auto FIXEDIMAGE_VIEW_STYLE = WB_3DLOOK | + WB_LEFT | WB_CENTER | WB_RIGHT | + WB_TOP | WB_VCENTER | WB_BOTTOM | + WB_SCALE; + +static Point ImplCalcPos( WinBits nStyle, const Point& rPos, + const Size& rObjSize, const Size& rWinSize ) +{ + tools::Long nX; + tools::Long nY; + + if ( nStyle & WB_LEFT ) + nX = 0; + else if ( nStyle & WB_RIGHT ) + nX = rWinSize.Width()-rObjSize.Width(); + else + nX = (rWinSize.Width()-rObjSize.Width())/2; + + if ( nStyle & WB_TOP ) + nY = 0; + else if ( nStyle & WB_BOTTOM ) + nY = rWinSize.Height()-rObjSize.Height(); + else + nY = (rWinSize.Height()-rObjSize.Height())/2; + + Point aPos( nX+rPos.X(), nY+rPos.Y() ); + return aPos; +} + +void FixedText::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle( nStyle ); + Control::ImplInit( pParent, nStyle, nullptr ); + ApplySettings(*GetOutDev()); +} + +WinBits FixedText::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + return nStyle; +} + +const vcl::Font& FixedText::GetCanonicalFont( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetLabelFont(); +} + +const Color& FixedText::GetCanonicalTextColor( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetLabelTextColor(); +} + +FixedText::FixedText( vcl::Window* pParent, WinBits nStyle ) + : Control(WindowType::FIXEDTEXT) + , m_nMaxWidthChars(-1) + , m_nMinWidthChars(-1) + , m_pMnemonicWindow(nullptr) +{ + ImplInit( pParent, nStyle ); +} + +DrawTextFlags FixedText::ImplGetTextStyle( WinBits nWinStyle ) +{ + DrawTextFlags nTextStyle = DrawTextFlags::Mnemonic | DrawTextFlags::EndEllipsis; + + if( ! (nWinStyle & WB_NOMULTILINE) ) + nTextStyle |= DrawTextFlags::MultiLine; + + if ( nWinStyle & WB_RIGHT ) + nTextStyle |= DrawTextFlags::Right; + else if ( nWinStyle & WB_CENTER ) + nTextStyle |= DrawTextFlags::Center; + else + nTextStyle |= DrawTextFlags::Left; + if ( nWinStyle & WB_BOTTOM ) + nTextStyle |= DrawTextFlags::Bottom; + else if ( nWinStyle & WB_VCENTER ) + nTextStyle |= DrawTextFlags::VCenter; + else + nTextStyle |= DrawTextFlags::Top; + if ( nWinStyle & WB_WORDBREAK ) + nTextStyle |= DrawTextFlags::WordBreak; + if ( nWinStyle & WB_NOLABEL ) + nTextStyle &= ~DrawTextFlags::Mnemonic; + + return nTextStyle; +} + +void FixedText::ImplDraw(OutputDevice* pDev, SystemTextColorFlags nSystemTextColorFlags, + const Point& rPos, const Size& rSize, + bool bFillLayout) const +{ + const StyleSettings& rStyleSettings = pDev->GetSettings().GetStyleSettings(); + WinBits nWinStyle = GetStyle(); + OUString aText(GetText()); + DrawTextFlags nTextStyle = FixedText::ImplGetTextStyle( nWinStyle ); + Point aPos = rPos; + + if ( nWinStyle & WB_EXTRAOFFSET ) + aPos.AdjustX(2 ); + + if ( nWinStyle & WB_PATHELLIPSIS ) + { + nTextStyle &= ~DrawTextFlags(DrawTextFlags::EndEllipsis | DrawTextFlags::MultiLine | DrawTextFlags::WordBreak); + nTextStyle |= DrawTextFlags::PathEllipsis; + } + if ( !IsEnabled() ) + nTextStyle |= DrawTextFlags::Disable; + if ( (nSystemTextColorFlags & SystemTextColorFlags::Mono) || + (rStyleSettings.GetOptions() & StyleSettingsOptions::Mono) ) + nTextStyle |= DrawTextFlags::Mono; + + if( bFillLayout ) + mxLayoutData->m_aDisplayText.clear(); + + const tools::Rectangle aRect(aPos, rSize); + DrawControlText(*pDev, aRect, aText, nTextStyle, + bFillLayout ? &mxLayoutData->m_aUnicodeBoundRects : nullptr, + bFillLayout ? &mxLayoutData->m_aDisplayText : nullptr); +} + +void FixedText::ApplySettings(vcl::RenderContext& rRenderContext) +{ + Control::ApplySettings(rRenderContext); + + vcl::Window* pParent = GetParent(); + bool bEnableTransparent = true; + if (!pParent->IsChildTransparentModeEnabled() || IsControlBackground()) + { + EnableChildTransparentMode(false); + SetParentClipMode(); + SetPaintTransparent(false); + + if (IsControlBackground()) + rRenderContext.SetBackground(GetControlBackground()); + else + rRenderContext.SetBackground(pParent->GetBackground()); + + if (rRenderContext.IsBackground()) + bEnableTransparent = false; + } + + if (bEnableTransparent) + { + EnableChildTransparentMode(); + SetParentClipMode(ParentClipMode::NoClip); + SetPaintTransparent(true); + rRenderContext.SetBackground(); + } +} + +void FixedText::Paint( vcl::RenderContext& rRenderContext, const tools::Rectangle& ) +{ + ImplDraw(&rRenderContext, SystemTextColorFlags::NONE, Point(), GetOutputSizePixel()); +} + +void FixedText::Draw( OutputDevice* pDev, const Point& rPos, + SystemTextColorFlags nFlags ) +{ + ApplySettings(*pDev); + + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + vcl::Font aFont = GetDrawPixelFont( pDev ); + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + if ( nFlags & SystemTextColorFlags::Mono ) + pDev->SetTextColor( COL_BLACK ); + else + pDev->SetTextColor( GetTextColor() ); + pDev->SetTextFillColor(); + + bool bBorder = (GetStyle() & WB_BORDER); + bool bBackground = IsControlBackground(); + if ( bBorder || bBackground ) + { + tools::Rectangle aRect( aPos, aSize ); + if ( bBorder ) + { + ImplDrawFrame( pDev, aRect ); + } + if ( bBackground ) + { + pDev->SetFillColor( GetControlBackground() ); + pDev->DrawRect( aRect ); + } + } + + ImplDraw( pDev, nFlags, aPos, aSize ); + pDev->Pop(); +} + +void FixedText::Resize() +{ + Control::Resize(); + Invalidate(); +} + +void FixedText::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( (nType == StateChangedType::Enable) || + (nType == StateChangedType::Text) || + (nType == StateChangedType::UpdateMode) ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetStyle() ) ); + if ( (GetPrevStyle() & FIXEDTEXT_VIEW_STYLE) != + (GetStyle() & FIXEDTEXT_VIEW_STYLE) ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + } + else if ( (nType == StateChangedType::Zoom) || + (nType == StateChangedType::ControlFont) ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +void FixedText::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +Size FixedText::getTextDimensions(Control const *pControl, const OUString &rTxt, tools::Long nMaxWidth) +{ + DrawTextFlags nStyle = ImplGetTextStyle( pControl->GetStyle() ); + if ( !( pControl->GetStyle() & WB_NOLABEL ) ) + nStyle |= DrawTextFlags::Mnemonic; + + return pControl->GetTextRect(tools::Rectangle( Point(), Size(nMaxWidth, 0x7fffffff)), + rTxt, nStyle).GetSize(); +} + +Size FixedText::CalcMinimumTextSize( Control const *pControl, tools::Long nMaxWidth ) +{ + Size aSize = getTextDimensions(pControl, pControl->GetText(), nMaxWidth); + + if ( pControl->GetStyle() & WB_EXTRAOFFSET ) + aSize.AdjustWidth(2 ); + + // GetTextRect cannot take an empty string + if ( aSize.Width() < 0 ) + aSize.setWidth( 0 ); + if ( aSize.Height() <= 0 ) + aSize.setHeight( pControl->GetTextHeight() ); + + return aSize; +} + +Size FixedText::CalcMinimumSize( tools::Long nMaxWidth ) const +{ + return CalcWindowSize( CalcMinimumTextSize ( this, nMaxWidth ) ); +} + +Size FixedText::GetOptimalSize() const +{ + sal_Int32 nMaxAvailWidth = 0x7fffffff; + if (m_nMaxWidthChars != -1) + { + OUStringBuffer aBuf(m_nMaxWidthChars); + comphelper::string::padToLength(aBuf, m_nMaxWidthChars, 'x'); + nMaxAvailWidth = getTextDimensions(this, + aBuf.makeStringAndClear(), 0x7fffffff).Width(); + } + Size aRet = CalcMinimumSize(nMaxAvailWidth); + if (m_nMinWidthChars != -1) + { + OUStringBuffer aBuf(m_nMinWidthChars); + comphelper::string::padToLength(aBuf, m_nMinWidthChars, 'x'); + Size aMinAllowed = getTextDimensions(this, + aBuf.makeStringAndClear(), 0x7fffffff); + aRet.setWidth(std::max(aMinAllowed.Width(), aRet.Width())); + } + return aRet; +} + +void FixedText::FillLayoutData() const +{ + mxLayoutData.emplace(); + ImplDraw(const_cast<FixedText*>(this)->GetOutDev(), SystemTextColorFlags::NONE, Point(), GetOutputSizePixel(), true); + //const_cast<FixedText*>(this)->Invalidate(); +} + +void FixedText::setMaxWidthChars(sal_Int32 nWidth) +{ + if (nWidth != m_nMaxWidthChars) + { + m_nMaxWidthChars = nWidth; + queue_resize(); + } +} + +void FixedText::setMinWidthChars(sal_Int32 nWidth) +{ + if (nWidth != m_nMinWidthChars) + { + m_nMinWidthChars = nWidth; + queue_resize(); + } +} + +bool FixedText::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "max-width-chars") + setMaxWidthChars(rValue.toInt32()); + else if (rKey == "width-chars") + setMinWidthChars(rValue.toInt32()); + else if (rKey == "ellipsize") + { + WinBits nBits = GetStyle(); + nBits &= ~WB_PATHELLIPSIS; + if (rValue != "none") + { + SAL_WARN_IF(rValue != "end", "vcl.layout", "Only endellipsis support for now"); + nBits |= WB_PATHELLIPSIS; + } + SetStyle(nBits); + } + else + return Control::set_property(rKey, rValue); + return true; +} + +vcl::Window* FixedText::getAccessibleRelationLabelFor() const +{ + vcl::Window *pWindow = Control::getAccessibleRelationLabelFor(); + if (pWindow) + return pWindow; + return get_mnemonic_widget(); +} + +void FixedText::set_mnemonic_widget(vcl::Window *pWindow) +{ + if (pWindow == m_pMnemonicWindow) + return; + if (m_pMnemonicWindow) + { + vcl::Window *pTempReEntryGuard = m_pMnemonicWindow; + m_pMnemonicWindow = nullptr; + pTempReEntryGuard->remove_mnemonic_label(this); + } + m_pMnemonicWindow = pWindow; + if (m_pMnemonicWindow) + m_pMnemonicWindow->add_mnemonic_label(this); +} + +FixedText::~FixedText() +{ + disposeOnce(); +} + +void FixedText::dispose() +{ + set_mnemonic_widget(nullptr); + m_pMnemonicWindow.clear(); + Control::dispose(); +} + +SelectableFixedText::SelectableFixedText(vcl::Window* pParent, WinBits nStyle) + : Edit(pParent, nStyle) +{ + // no border + SetBorderStyle( WindowBorderStyle::NOBORDER ); + // read-only + SetReadOnly(); + // make it transparent + SetPaintTransparent(true); + SetControlBackground(); +} + +void SelectableFixedText::ApplySettings(vcl::RenderContext& rRenderContext) +{ + rRenderContext.SetBackground(); +} + +void SelectableFixedText::LoseFocus() +{ + Edit::LoseFocus(); + // clear cursor + Invalidate(); +} + +void SelectableFixedText::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Edit::DumpAsPropertyTree(rJsonWriter); + rJsonWriter.put("type", "fixedtext"); + rJsonWriter.put("selectable", "true"); +} + +void FixedLine::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle( nStyle ); + Control::ImplInit( pParent, nStyle, nullptr ); + ApplySettings(*GetOutDev()); +} + +WinBits FixedLine::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + return nStyle; +} + +const vcl::Font& FixedLine::GetCanonicalFont( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetGroupFont(); +} + +const Color& FixedLine::GetCanonicalTextColor( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetGroupTextColor(); +} + +void FixedLine::ImplDraw(vcl::RenderContext& rRenderContext) +{ + // we need to measure according to the window, not according to the + // RenderContext we paint to + Size aOutSize = GetOutputSizePixel(); + + OUString aText = GetText(); + WinBits nWinStyle = GetStyle(); + + DecorationView aDecoView(&rRenderContext); + if (aText.isEmpty()) + { + if (nWinStyle & WB_VERT) + { + tools::Long nX = (aOutSize.Width() - 1) / 2; + aDecoView.DrawSeparator(Point(nX, 0), Point(nX, aOutSize.Height() - 1)); + } + else + { + tools::Long nY = (aOutSize.Height() - 1) / 2; + aDecoView.DrawSeparator(Point(0, nY), Point(aOutSize.Width() - 1, nY), false); + } + } + else if (nWinStyle & WB_VERT) + { + tools::Long nWidth = rRenderContext.GetTextWidth(aText); + rRenderContext.Push(vcl::PushFlags::FONT); + vcl::Font aFont(rRenderContext.GetFont()); + aFont.SetOrientation(900_deg10); + SetFont(aFont); + Point aStartPt(aOutSize.Width() / 2, aOutSize.Height() - 1); + if (nWinStyle & WB_VCENTER) + aStartPt.AdjustY( -((aOutSize.Height() - nWidth) / 2) ); + Point aTextPt(aStartPt); + aTextPt.AdjustX( -(GetTextHeight() / 2) ); + rRenderContext.DrawText(aTextPt, aText, 0, aText.getLength()); + rRenderContext.Pop(); + if (aOutSize.Height() - aStartPt.Y() > FIXEDLINE_TEXT_BORDER) + aDecoView.DrawSeparator(Point(aStartPt.X(), aStartPt.Y() + FIXEDLINE_TEXT_BORDER), + Point(aStartPt.X(), aOutSize.Height() - 1)); + if (aStartPt.Y() - nWidth - FIXEDLINE_TEXT_BORDER > 0) + aDecoView.DrawSeparator(Point(aStartPt.X(), 0), + Point(aStartPt.X(), aStartPt.Y() - nWidth - FIXEDLINE_TEXT_BORDER)); + } + else + { + DrawTextFlags nStyle = DrawTextFlags::Mnemonic | DrawTextFlags::Left | DrawTextFlags::VCenter | DrawTextFlags::EndEllipsis; + tools::Rectangle aRect(0, 0, aOutSize.Width(), aOutSize.Height()); + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + if (nWinStyle & WB_CENTER) + nStyle |= DrawTextFlags::Center; + + if (!IsEnabled()) + nStyle |= DrawTextFlags::Disable; + if (GetStyle() & WB_NOLABEL) + nStyle &= ~DrawTextFlags::Mnemonic; + if (rStyleSettings.GetOptions() & StyleSettingsOptions::Mono) + nStyle |= DrawTextFlags::Mono; + + aRect = DrawControlText(*GetOutDev(), aRect, aText, nStyle, nullptr, nullptr); + + tools::Long nTop = aRect.Top() + ((aRect.GetHeight() - 1) / 2); + aDecoView.DrawSeparator(Point(aRect.Right() + FIXEDLINE_TEXT_BORDER, nTop), Point(aOutSize.Width() - 1, nTop), false); + if (aRect.Left() > FIXEDLINE_TEXT_BORDER) + aDecoView.DrawSeparator(Point(0, nTop), Point(aRect.Left() - FIXEDLINE_TEXT_BORDER, nTop), false); + } +} + +FixedLine::FixedLine( vcl::Window* pParent, WinBits nStyle ) : + Control( WindowType::FIXEDLINE ) +{ + ImplInit( pParent, nStyle ); + SetSizePixel( Size( 2, 2 ) ); +} + +void FixedLine::FillLayoutData() const +{ + mxLayoutData.emplace(); + const_cast<FixedLine*>(this)->Invalidate(); +} + +void FixedLine::ApplySettings(vcl::RenderContext& rRenderContext) +{ + Control::ApplySettings(rRenderContext); + + vcl::Window* pParent = GetParent(); + if (pParent->IsChildTransparentModeEnabled() && !IsControlBackground()) + { + EnableChildTransparentMode(); + SetParentClipMode(ParentClipMode::NoClip); + SetPaintTransparent(true); + rRenderContext.SetBackground(); + } + else + { + EnableChildTransparentMode(false); + SetParentClipMode(); + SetPaintTransparent(false); + + if (IsControlBackground()) + rRenderContext.SetBackground(GetControlBackground()); + else + rRenderContext.SetBackground(pParent->GetBackground()); + } +} + +void FixedLine::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) +{ + ImplDraw(rRenderContext); +} + +void FixedLine::Draw( OutputDevice*, const Point&, SystemTextColorFlags ) +{ +} + +void FixedLine::Resize() +{ + Control::Resize(); + Invalidate(); +} + +void FixedLine::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( (nType == StateChangedType::Enable) || + (nType == StateChangedType::Text) || + (nType == StateChangedType::UpdateMode) ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetStyle() ) ); + if ( (GetPrevStyle() & FIXEDLINE_VIEW_STYLE) != + (GetStyle() & FIXEDLINE_VIEW_STYLE) ) + Invalidate(); + } + else if ( (nType == StateChangedType::Zoom) || + (nType == StateChangedType::Style) || + (nType == StateChangedType::ControlFont) ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +void FixedLine::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +Size FixedLine::GetOptimalSize() const +{ + return CalcWindowSize( FixedText::CalcMinimumTextSize ( this ) ); +} + +void FixedLine::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Control::DumpAsPropertyTree(rJsonWriter); + rJsonWriter.put("type", "separator"); + rJsonWriter.put("orientation", (GetStyle() & WB_VERT) ? "vertical" : "horizontal"); +} + +void FixedBitmap::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle( nStyle ); + Control::ImplInit( pParent, nStyle, nullptr ); + ApplySettings(*GetOutDev()); +} + +WinBits FixedBitmap::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + return nStyle; +} + +FixedBitmap::FixedBitmap( vcl::Window* pParent, WinBits nStyle ) : + Control( WindowType::FIXEDBITMAP ) +{ + ImplInit( pParent, nStyle ); +} + +void FixedBitmap::ImplDraw( OutputDevice* pDev, const Point& rPos, const Size& rSize ) +{ + // do we have a Bitmap? + if ( !maBitmap.IsEmpty() ) + { + if ( GetStyle() & WB_SCALE ) + pDev->DrawBitmapEx( rPos, rSize, maBitmap ); + else + { + Point aPos = ImplCalcPos( GetStyle(), rPos, maBitmap.GetSizePixel(), rSize ); + pDev->DrawBitmapEx( aPos, maBitmap ); + } + } +} + +void FixedBitmap::ApplySettings(vcl::RenderContext& rRenderContext) +{ + vcl::Window* pParent = GetParent(); + if (pParent->IsChildTransparentModeEnabled() && !IsControlBackground()) + { + EnableChildTransparentMode(); + SetParentClipMode(ParentClipMode::NoClip); + SetPaintTransparent(true); + rRenderContext.SetBackground(); + } + else + { + EnableChildTransparentMode(false); + SetParentClipMode(); + SetPaintTransparent(false); + + if (IsControlBackground()) + rRenderContext.SetBackground(GetControlBackground()); + else + rRenderContext.SetBackground(pParent->GetBackground()); + } +} + +void FixedBitmap::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) +{ + ImplDraw(&rRenderContext, Point(), GetOutputSizePixel()); +} + +void FixedBitmap::Draw( OutputDevice* pDev, const Point& rPos, + SystemTextColorFlags ) +{ + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + tools::Rectangle aRect( aPos, aSize ); + + pDev->Push(); + pDev->SetMapMode(); + + // Border + if ( GetStyle() & WB_BORDER ) + { + DecorationView aDecoView( pDev ); + aRect = aDecoView.DrawFrame( aRect, DrawFrameStyle::DoubleIn ); + } + pDev->IntersectClipRegion( aRect ); + ImplDraw( pDev, aRect.TopLeft(), aRect.GetSize() ); + + pDev->Pop(); +} + +void FixedBitmap::Resize() +{ + Control::Resize(); + Invalidate(); +} + +void FixedBitmap::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( (nType == StateChangedType::Data) || + (nType == StateChangedType::UpdateMode) ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetStyle() ) ); + if ( (GetPrevStyle() & FIXEDBITMAP_VIEW_STYLE) != + (GetStyle() & FIXEDBITMAP_VIEW_STYLE) ) + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +void FixedBitmap::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +void FixedBitmap::SetBitmap( const BitmapEx& rBitmap ) +{ + maBitmap = rBitmap; + CompatStateChanged( StateChangedType::Data ); + queue_resize(); +} + +void FixedImage::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle( nStyle ); + Control::ImplInit( pParent, nStyle, nullptr ); + ApplySettings(*GetOutDev()); +} + +WinBits FixedImage::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + return nStyle; +} + +FixedImage::FixedImage( vcl::Window* pParent, WinBits nStyle ) : + Control( WindowType::FIXEDIMAGE ) +{ + ImplInit( pParent, nStyle ); +} + +void FixedImage::ImplDraw( OutputDevice* pDev, + const Point& rPos, const Size& rSize ) +{ + DrawImageFlags nStyle = DrawImageFlags::NONE; + if ( !IsEnabled() ) + nStyle |= DrawImageFlags::Disable; + + Image *pImage = &maImage; + + // do we have an image? + if ( !(!(*pImage)) ) + { + if ( GetStyle() & WB_SCALE ) + pDev->DrawImage( rPos, rSize, *pImage, nStyle ); + else + { + Point aPos = ImplCalcPos( GetStyle(), rPos, pImage->GetSizePixel(), rSize ); + pDev->DrawImage( aPos, *pImage, nStyle ); + } + } +} + +void FixedImage::ApplySettings(vcl::RenderContext& rRenderContext) +{ + vcl::Window* pParent = GetParent(); + if (pParent && pParent->IsChildTransparentModeEnabled() && !IsControlBackground()) + { + EnableChildTransparentMode(); + SetParentClipMode(ParentClipMode::NoClip); + SetPaintTransparent(true); + rRenderContext.SetBackground(); + } + else + { + EnableChildTransparentMode(false); + SetParentClipMode(); + SetPaintTransparent(false); + + if (IsControlBackground()) + rRenderContext.SetBackground(GetControlBackground()); + else if (pParent) + rRenderContext.SetBackground(pParent->GetBackground()); + } +} + + +void FixedImage::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) +{ + ImplDraw(&rRenderContext, Point(), GetOutputSizePixel()); +} + +Size FixedImage::GetOptimalSize() const +{ + return maImage.GetSizePixel(); +} + +void FixedImage::Draw( OutputDevice* pDev, const Point& rPos, + SystemTextColorFlags ) +{ + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + tools::Rectangle aRect( aPos, aSize ); + + pDev->Push(); + pDev->SetMapMode(); + + // Border + if ( GetStyle() & WB_BORDER ) + { + ImplDrawFrame( pDev, aRect ); + } + pDev->IntersectClipRegion( aRect ); + ImplDraw( pDev, aRect.TopLeft(), aRect.GetSize() ); + + pDev->Pop(); +} + +void FixedImage::Resize() +{ + Control::Resize(); + Invalidate(); +} + +void FixedImage::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( (nType == StateChangedType::Enable) || + (nType == StateChangedType::Data) || + (nType == StateChangedType::UpdateMode) ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetStyle() ) ); + if ( (GetPrevStyle() & FIXEDIMAGE_VIEW_STYLE) != + (GetStyle() & FIXEDIMAGE_VIEW_STYLE) ) + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +void FixedImage::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } +} + +void FixedImage::SetImage( const Image& rImage ) +{ + if ( rImage != maImage ) + { + maImage = rImage; + CompatStateChanged( StateChangedType::Data ); + queue_resize(); + } +} + +Image FixedImage::loadThemeImage(const OUString &rFileName) +{ + return Image(StockImage::Yes, rFileName); +} + +bool FixedImage::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "icon-size") + { + WinBits nBits = GetStyle(); + nBits &= ~WB_SMALLSTYLE; + if (rValue == "2") + nBits |= WB_SMALLSTYLE; + SetStyle(nBits); + } + else + return Control::set_property(rKey, rValue); + return true; +} + +void FixedImage::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Control::DumpAsPropertyTree(rJsonWriter); + rJsonWriter.put("id", get_id()); + rJsonWriter.put("type", "image"); + if (!!maImage) + { + SvMemoryStream aOStm(6535, 6535); + if(GraphicConverter::Export(aOStm, maImage.GetBitmapEx(), ConvertDataFormat::PNG) == ERRCODE_NONE) + { + css::uno::Sequence<sal_Int8> aSeq( static_cast<sal_Int8 const *>(aOStm.GetData()), aOStm.Tell()); + OUStringBuffer aBuffer("data:image/png;base64,"); + ::comphelper::Base64::encode(aBuffer, aSeq); + rJsonWriter.put("image", aBuffer.makeStringAndClear()); + } + } +} + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/fixedhyper.cxx b/vcl/source/control/fixedhyper.cxx new file mode 100644 index 000000000..8a451d2fa --- /dev/null +++ b/vcl/source/control/fixedhyper.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 <vcl/event.hxx> +#include <vcl/toolkit/fixedhyper.hxx> +#include <vcl/settings.hxx> +#include <vcl/svapp.hxx> +#include <vcl/weld.hxx> +#include <vcl/ptrstyle.hxx> +#include <comphelper/anytostring.hxx> +#include <comphelper/processfactory.hxx> +#include <cppuhelper/exc_hlp.hxx> + +#include <com/sun/star/system/XSystemShellExecute.hpp> +#include <com/sun/star/system/SystemShellExecuteFlags.hpp> +#include <com/sun/star/system/SystemShellExecute.hpp> + +using namespace css; + +FixedHyperlink::FixedHyperlink(vcl::Window* pParent, WinBits nWinStyle) + : FixedText(pParent, nWinStyle) + , m_nTextLen(0) + , m_aOldPointer(PointerStyle::Arrow) +{ + Initialize(); +} + +void FixedHyperlink::Initialize() +{ + // saves the old pointer + m_aOldPointer = GetPointer(); + // changes the font + vcl::Font aFont = GetControlFont( ); + // to underline + aFont.SetUnderline( LINESTYLE_SINGLE ); + SetControlFont( aFont ); + // changes the color to link color + SetControlForeground( Application::GetSettings().GetStyleSettings().GetLinkColor() ); + // calculates text len + m_nTextLen = GetOutDev()->GetCtrlTextWidth( GetText() ); + + SetClickHdl(LINK(this, FixedHyperlink, HandleClick)); +} + +bool FixedHyperlink::ImplIsOverText(Point aPosition) const +{ + Size aSize = GetOutputSizePixel(); + + bool bIsOver = false; + + if (GetStyle() & WB_RIGHT) + { + return aPosition.X() > (aSize.Width() - m_nTextLen); + } + else if (GetStyle() & WB_CENTER) + { + bIsOver = aPosition.X() > (aSize.Width() / 2 - m_nTextLen / 2) && + aPosition.X() < (aSize.Width() / 2 + m_nTextLen / 2); + } + else + { + bIsOver = aPosition.X() < m_nTextLen; + } + + return bIsOver; +} + +void FixedHyperlink::MouseMove( const MouseEvent& rMEvt ) +{ + // changes the pointer if the control is enabled and the mouse is over the text. + if ( !rMEvt.IsLeaveWindow() && IsEnabled() && ImplIsOverText(GetPointerPosPixel()) ) + SetPointer( PointerStyle::RefHand ); + else + SetPointer( m_aOldPointer ); +} + +void FixedHyperlink::MouseButtonUp( const MouseEvent& ) +{ + // calls the link if the control is enabled and the mouse is over the text. + if ( IsEnabled() && ImplIsOverText(GetPointerPosPixel()) ) + ImplCallEventListenersAndHandler( VclEventId::ButtonClick, [this] () { m_aClickHdl.Call(*this); } ); +} + +void FixedHyperlink::RequestHelp( const HelpEvent& rHEvt ) +{ + if ( IsEnabled() && ImplIsOverText(GetPointerPosPixel()) ) + FixedText::RequestHelp( rHEvt ); +} + +void FixedHyperlink::GetFocus() +{ + Size aSize = GetSizePixel(); + tools::Rectangle aFocusRect(Point(1, 1), Size(m_nTextLen + 4, aSize.Height() - 2)); + if (GetStyle() & WB_RIGHT) + aFocusRect.Move(aSize.Width() - aFocusRect.getWidth(), 0); + else if (GetStyle() & WB_CENTER) + aFocusRect.Move((aSize.Width() - aFocusRect.getWidth()) / 2, 0); + + Invalidate(aFocusRect); + ShowFocus(aFocusRect); +} + +void FixedHyperlink::LoseFocus() +{ + SetTextColor( GetControlForeground() ); + Invalidate(tools::Rectangle(Point(), GetSizePixel())); + HideFocus(); +} + +void FixedHyperlink::KeyInput( const KeyEvent& rKEvt ) +{ + switch ( rKEvt.GetKeyCode().GetCode() ) + { + case KEY_SPACE: + case KEY_RETURN: + m_aClickHdl.Call( *this ); + break; + + default: + FixedText::KeyInput( rKEvt ); + } +} + +void FixedHyperlink::SetURL( const OUString& rNewURL ) +{ + m_sURL = rNewURL; + SetQuickHelpText( m_sURL ); +} + + +void FixedHyperlink::SetText(const OUString& rNewDescription) +{ + FixedText::SetText(rNewDescription); + m_nTextLen = GetOutDev()->GetCtrlTextWidth(GetText()); +} + +bool FixedHyperlink::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "uri") + SetURL(rValue); + else + return FixedText::set_property(rKey, rValue); + return true; +} + +IMPL_LINK(FixedHyperlink, HandleClick, FixedHyperlink&, rHyperlink, void) +{ + if ( rHyperlink.m_sURL.isEmpty() ) // Nothing to do, when the URL is empty + return; + + try + { + uno::Reference< system::XSystemShellExecute > xSystemShellExecute( + system::SystemShellExecute::create(comphelper::getProcessComponentContext())); + //throws css::lang::IllegalArgumentException, css::system::SystemShellExecuteException + xSystemShellExecute->execute( rHyperlink.m_sURL, OUString(), system::SystemShellExecuteFlags::URIS_ONLY ); + } + catch ( const uno::Exception& ) + { + uno::Any exc(cppu::getCaughtException()); + OUString msg(comphelper::anyToString(exc)); + SolarMutexGuard g; + std::shared_ptr<weld::MessageDialog> xErrorBox( + Application::CreateMessageDialog(GetFrameWeld(), VclMessageType::Error, VclButtonsType::Ok, msg)); + xErrorBox->set_title(rHyperlink.GetText()); + xErrorBox->runAsync(xErrorBox, [](sal_Int32){}); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/fmtfield.cxx b/vcl/source/control/fmtfield.cxx new file mode 100644 index 000000000..a88b6a9bb --- /dev/null +++ b/vcl/source/control/fmtfield.cxx @@ -0,0 +1,1367 @@ +/* -*- 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 <tools/debug.hxx> +#include <boost/property_tree/json_parser.hpp> +#include <comphelper/processfactory.hxx> +#include <comphelper/string.hxx> +#include <unotools/localedatawrapper.hxx> +#include <vcl/builder.hxx> +#include <vcl/event.hxx> +#include <vcl/settings.hxx> +#include <vcl/commandevent.hxx> +#include <svl/zformat.hxx> +#include <vcl/toolkit/fmtfield.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <vcl/uitest/formattedfielduiobject.hxx> +#include <vcl/weld.hxx> +#include <i18nlangtag/languagetag.hxx> +#include <unotools/syslocale.hxx> +#include <limits> +#include <map> +#include <rtl/math.hxx> +#include <rtl/ustrbuf.hxx> +#include <sal/log.hxx> +#include <svl/numformat.hxx> +#include <osl/diagnose.h> +#include <tools/json_writer.hxx> + +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::util; + +// hmm. No support for regular expression. Well, I always (not really :) wanted to write a finite automat +// so here comes a finite automat ... + +namespace validation +{ + static void lcl_insertStopTransition( StateTransitions& _rRow ) + { + _rRow.insert( Transition( '_', END ) ); + } + + static void lcl_insertStartExponentTransition( StateTransitions& _rRow ) + { + _rRow.insert( Transition( 'e', EXPONENT_START ) ); + } + + static void lcl_insertSignTransitions( StateTransitions& _rRow, const State eNextState ) + { + _rRow.insert( Transition( '-', eNextState ) ); + _rRow.insert( Transition( '+', eNextState ) ); + } + + static void lcl_insertDigitTransitions( StateTransitions& _rRow, const State eNextState ) + { + for ( sal_Unicode aChar = '0'; aChar <= '9'; ++aChar ) + _rRow.insert( Transition( aChar, eNextState ) ); + } + + static void lcl_insertCommonPreCommaTransitions( StateTransitions& _rRow, const sal_Unicode _cThSep, const sal_Unicode _cDecSep ) + { + // digits are allowed + lcl_insertDigitTransitions( _rRow, DIGIT_PRE_COMMA ); + + // the thousand separator is allowed + _rRow.insert( Transition( _cThSep, DIGIT_PRE_COMMA ) ); + + // a comma is allowed + _rRow.insert( Transition( _cDecSep, DIGIT_POST_COMMA ) ); + } + + NumberValidator::NumberValidator( const sal_Unicode _cThSep, const sal_Unicode _cDecSep ) + { + // build up our transition table + + // how to proceed from START + { + StateTransitions& rRow = m_aTransitions[ START ]; + rRow.insert( Transition( '_', NUM_START ) ); + // if we encounter the normalizing character, we want to proceed with the number + } + + // how to proceed from NUM_START + { + StateTransitions& rRow = m_aTransitions[ NUM_START ]; + + // a sign is allowed + lcl_insertSignTransitions( rRow, DIGIT_PRE_COMMA ); + + // common transitions for the two pre-comma states + lcl_insertCommonPreCommaTransitions( rRow, _cThSep, _cDecSep ); + + // the exponent may start here + // (this would mean string like "_+e10_", but this is a valid fragment, though no valid number) + lcl_insertStartExponentTransition( rRow ); + } + + // how to proceed from DIGIT_PRE_COMMA + { + StateTransitions& rRow = m_aTransitions[ DIGIT_PRE_COMMA ]; + + // common transitions for the two pre-comma states + lcl_insertCommonPreCommaTransitions( rRow, _cThSep, _cDecSep ); + + // the exponent may start here + lcl_insertStartExponentTransition( rRow ); + + // the final transition indicating the end of the string + // (if there is no comma and no post-comma, then the string may end here) + lcl_insertStopTransition( rRow ); + } + + // how to proceed from DIGIT_POST_COMMA + { + StateTransitions& rRow = m_aTransitions[ DIGIT_POST_COMMA ]; + + // there might be digits, which would keep the state at DIGIT_POST_COMMA + lcl_insertDigitTransitions( rRow, DIGIT_POST_COMMA ); + + // the exponent may start here + lcl_insertStartExponentTransition( rRow ); + + // the string may end here + lcl_insertStopTransition( rRow ); + } + + // how to proceed from EXPONENT_START + { + StateTransitions& rRow = m_aTransitions[ EXPONENT_START ]; + + // there may be a sign + lcl_insertSignTransitions( rRow, EXPONENT_DIGIT ); + + // there may be digits + lcl_insertDigitTransitions( rRow, EXPONENT_DIGIT ); + + // the string may end here + lcl_insertStopTransition( rRow ); + } + + // how to proceed from EXPONENT_DIGIT + { + StateTransitions& rRow = m_aTransitions[ EXPONENT_DIGIT ]; + + // there may be digits + lcl_insertDigitTransitions( rRow, EXPONENT_DIGIT ); + + // the string may end here + lcl_insertStopTransition( rRow ); + } + + // how to proceed from END + { + /*StateTransitions& rRow =*/ m_aTransitions[ EXPONENT_DIGIT ]; + // no valid transition to leave this state + // (note that we, for consistency, nevertheless want to have a row in the table) + } + } + + bool NumberValidator::implValidateNormalized( const OUString& _rText ) + { + const sal_Unicode* pCheckPos = _rText.getStr(); + State eCurrentState = START; + + while ( END != eCurrentState ) + { + // look up the transition row for the current state + TransitionTable::const_iterator aRow = m_aTransitions.find( eCurrentState ); + DBG_ASSERT( m_aTransitions.end() != aRow, + "NumberValidator::implValidateNormalized: invalid transition table (row not found)!" ); + + if ( m_aTransitions.end() != aRow ) + { + // look up the current character in this row + StateTransitions::const_iterator aTransition = aRow->second.find( *pCheckPos ); + if ( aRow->second.end() != aTransition ) + { + // there is a valid transition for this character + eCurrentState = aTransition->second; + ++pCheckPos; + continue; + } + } + + // if we're here, there is no valid transition + break; + } + + DBG_ASSERT( ( END != eCurrentState ) || ( 0 == *pCheckPos ), + "NumberValidator::implValidateNormalized: inconsistency!" ); + // if we're at END, then the string should be done, too - the string should be normalized, means ending + // a "_" and not containing any other "_" (except at the start), and "_" is the only possibility + // to reach the END state + + // the string is valid if and only if we reached the final state + return ( END == eCurrentState ); + } + + bool NumberValidator::isValidNumericFragment( std::u16string_view _rText ) + { + if ( _rText.empty() ) + // empty strings are always allowed + return true; + + // normalize the string + OUString sNormalized = OUString::Concat("_") + _rText + "_"; + + return implValidateNormalized( sNormalized ); + } +} + +SvNumberFormatter* Formatter::StaticFormatter::s_cFormatter = nullptr; +sal_uLong Formatter::StaticFormatter::s_nReferences = 0; + +SvNumberFormatter* Formatter::StaticFormatter::GetFormatter() +{ + if (!s_cFormatter) + { + // get the Office's locale and translate + LanguageType eSysLanguage = SvtSysLocale().GetLanguageTag().getLanguageType( false); + s_cFormatter = new SvNumberFormatter( + ::comphelper::getProcessComponentContext(), + eSysLanguage); + } + return s_cFormatter; +} + +Formatter::StaticFormatter::StaticFormatter() +{ + ++s_nReferences; +} + +Formatter::StaticFormatter::~StaticFormatter() +{ + if (--s_nReferences == 0) + { + delete s_cFormatter; + s_cFormatter = nullptr; + } +} + +Formatter::Formatter() + :m_aLastSelection(0,0) + ,m_dMinValue(0) + ,m_dMaxValue(0) + ,m_bHasMin(false) + ,m_bHasMax(false) + ,m_bWrapOnLimits(false) + ,m_bStrictFormat(true) + ,m_bEnableEmptyField(true) + ,m_bAutoColor(false) + ,m_bEnableNaN(false) + ,m_bDisableRemainderFactor(false) + ,m_ValueState(valueDirty) + ,m_dCurrentValue(0) + ,m_dDefaultValue(0) + ,m_nFormatKey(0) + ,m_pFormatter(nullptr) + ,m_dSpinSize(1) + ,m_dSpinFirst(-1000000) + ,m_dSpinLast(1000000) + ,m_bTreatAsNumber(true) + ,m_pLastOutputColor(nullptr) + ,m_bUseInputStringForFormatting(false) +{ +} + +Formatter::~Formatter() +{ +} + +void Formatter::SetFieldText(const OUString& rStr, const Selection& rNewSelection) +{ + SetEntryText(rStr, rNewSelection); + m_ValueState = valueDirty; +} + +void Formatter::SetTextFormatted(const OUString& rStr) +{ + SAL_INFO_IF(GetOrCreateFormatter()->IsTextFormat(m_nFormatKey), "svtools", + "FormattedField::SetTextFormatted : valid only with text formats !"); + + m_sCurrentTextValue = rStr; + + OUString sFormatted; + double dNumber = 0.0; + // IsNumberFormat changes the format key parameter + sal_uInt32 nTempFormatKey = static_cast< sal_uInt32 >( m_nFormatKey ); + if( IsUsingInputStringForFormatting() && + GetOrCreateFormatter()->IsNumberFormat(m_sCurrentTextValue, nTempFormatKey, dNumber) ) + { + GetOrCreateFormatter()->GetInputLineString(dNumber, m_nFormatKey, sFormatted); + } + else + { + GetOrCreateFormatter()->GetOutputString(m_sCurrentTextValue, + m_nFormatKey, + sFormatted, + &m_pLastOutputColor); + } + + // calculate the new selection + Selection aSel(GetEntrySelection()); + Selection aNewSel(aSel); + aNewSel.Justify(); + sal_Int32 nNewLen = sFormatted.getLength(); + sal_Int32 nCurrentLen = GetEntryText().getLength(); + if ((nNewLen > nCurrentLen) && (aNewSel.Max() == nCurrentLen)) + { // the new text is longer and the cursor was behind the last char (of the old text) + if (aNewSel.Min() == 0) + { // the whole text was selected -> select the new text on the whole, too + aNewSel.Max() = nNewLen; + if (!nCurrentLen) + { // there wasn't really a previous selection (as there was no previous text), we're setting a new one -> check the selection options + SelectionOptions nSelOptions = GetEntrySelectionOptions(); + if (nSelOptions & SelectionOptions::ShowFirst) + { // selection should be from right to left -> swap min and max + aNewSel.Min() = aNewSel.Max(); + aNewSel.Max() = 0; + } + } + } + else if (aNewSel.Max() == aNewSel.Min()) + { // there was no selection -> set the cursor behind the new last char + aNewSel.Max() = nNewLen; + aNewSel.Min() = nNewLen; + } + } + else if (aNewSel.Max() > nNewLen) + aNewSel.Max() = nNewLen; + else + aNewSel = aSel; // don't use the justified version + SetEntryText(sFormatted, aNewSel); + m_ValueState = valueString; +} + +OUString const & Formatter::GetTextValue() const +{ + if (m_ValueState != valueString ) + { + const_cast<Formatter*>(this)->m_sCurrentTextValue = GetEntryText(); + const_cast<Formatter*>(this)->m_ValueState = valueString; + } + return m_sCurrentTextValue; +} + +void Formatter::EnableNotANumber(bool _bEnable) +{ + if ( m_bEnableNaN == _bEnable ) + return; + + m_bEnableNaN = _bEnable; +} + +void Formatter::SetAutoColor(bool _bAutomatic) +{ + if (_bAutomatic == m_bAutoColor) + return; + + m_bAutoColor = _bAutomatic; + if (m_bAutoColor) + { + // if auto color is switched on, adjust the current text color, too + SetEntryTextColor(m_pLastOutputColor); + } +} + +void Formatter::Modify(bool makeValueDirty) +{ + if (!IsStrictFormat()) + { + if(makeValueDirty) + m_ValueState = valueDirty; + FieldModified(); + return; + } + + OUString sCheck = GetEntryText(); + if (CheckText(sCheck)) + { + m_sLastValidText = sCheck; + m_aLastSelection = GetEntrySelection(); + if(makeValueDirty) + m_ValueState = valueDirty; + } + else + { + ImplSetTextImpl(m_sLastValidText, &m_aLastSelection); + } + + FieldModified(); +} + +void Formatter::ImplSetTextImpl(const OUString& rNew, Selection const * pNewSel) +{ + if (m_bAutoColor) + SetEntryTextColor(m_pLastOutputColor); + + if (pNewSel) + SetEntryText(rNew, *pNewSel); + else + { + Selection aSel(GetEntrySelection()); + aSel.Justify(); + + sal_Int32 nNewLen = rNew.getLength(); + sal_Int32 nCurrentLen = GetEntryText().getLength(); + + if ((nNewLen > nCurrentLen) && (aSel.Max() == nCurrentLen)) + { // new text is longer and the cursor is behind the last char + if (aSel.Min() == 0) + { + if (!nCurrentLen) + { // there wasn't really a previous selection (as there was no previous text) + aSel.Max() = 0; + } + else + { // the whole text was selected -> select the new text on the whole, too + aSel.Max() = nNewLen; + } + } + else if (aSel.Max() == aSel.Min()) + { // there was no selection -> set the cursor behind the new last char + aSel.Max() = nNewLen; + aSel.Min() = nNewLen; + } + } + else if (aSel.Max() > nNewLen) + aSel.Max() = nNewLen; + SetEntryText(rNew, aSel); + } + + m_ValueState = valueDirty; // not always necessary, but better re-evaluate for safety reasons +} + +void Formatter::ImplSetFormatKey(sal_uLong nFormatKey) +{ + m_nFormatKey = nFormatKey; + bool bNeedFormatter = (m_pFormatter == nullptr) && (nFormatKey != 0); + if (bNeedFormatter) + { + GetOrCreateFormatter(); // this creates a standard formatter + + // It might happen that the standard formatter makes no sense here, but it takes a default + // format. Thus, it is possible to set one of the other standard keys (which are spanning + // across multiple formatters). + m_nFormatKey = nFormatKey; + // When calling SetFormatKey without a formatter, the key must be one of the standard values + // that is available for all formatters (and, thus, also in this new one). + DBG_ASSERT(m_pFormatter->GetEntry(nFormatKey) != nullptr, "FormattedField::ImplSetFormatKey : invalid format key !"); + } +} + +void Formatter::SetFormatKey(sal_uLong nFormatKey) +{ + bool bNoFormatter = (m_pFormatter == nullptr); + ImplSetFormatKey(nFormatKey); + FormatChanged((bNoFormatter && (m_pFormatter != nullptr)) ? FORMAT_CHANGE_TYPE::FORMATTER : FORMAT_CHANGE_TYPE::KEYONLY); +} + +void Formatter::SetFormatter(SvNumberFormatter* pFormatter, bool bResetFormat) +{ + + if (bResetFormat) + { + m_pFormatter = pFormatter; + + // calc the default format key from the Office's UI locale + if ( m_pFormatter ) + { + // get the Office's locale and translate + LanguageType eSysLanguage = SvtSysLocale().GetLanguageTag().getLanguageType( false); + // get the standard numeric format for this language + m_nFormatKey = m_pFormatter->GetStandardFormat( SvNumFormatType::NUMBER, eSysLanguage ); + } + else + m_nFormatKey = 0; + } + else + { + LanguageType aOldLang; + OUString sOldFormat = GetFormat(aOldLang); + + sal_uInt32 nDestKey = pFormatter->TestNewString(sOldFormat); + if (nDestKey == NUMBERFORMAT_ENTRY_NOT_FOUND) + { + // language of the new formatter + const SvNumberformat* pDefaultEntry = pFormatter->GetEntry(0); + LanguageType aNewLang = pDefaultEntry ? pDefaultEntry->GetLanguage() : LANGUAGE_DONTKNOW; + + // convert the old format string into the new language + sal_Int32 nCheckPos; + SvNumFormatType nType; + pFormatter->PutandConvertEntry(sOldFormat, nCheckPos, nType, nDestKey, aOldLang, aNewLang, true); + m_nFormatKey = nDestKey; + } + m_pFormatter = pFormatter; + } + + FormatChanged(FORMAT_CHANGE_TYPE::FORMATTER); +} + +OUString Formatter::GetFormat(LanguageType& eLang) const +{ + const SvNumberformat* pFormatEntry = GetOrCreateFormatter()->GetEntry(m_nFormatKey); + DBG_ASSERT(pFormatEntry != nullptr, "FormattedField::GetFormat: no number format for the given format key."); + OUString sFormatString = pFormatEntry ? pFormatEntry->GetFormatstring() : OUString(); + eLang = pFormatEntry ? pFormatEntry->GetLanguage() : LANGUAGE_DONTKNOW; + + return sFormatString; +} + +bool Formatter::SetFormat(const OUString& rFormatString, LanguageType eLang) +{ + sal_uInt32 nNewKey = GetOrCreateFormatter()->TestNewString(rFormatString, eLang); + if (nNewKey == NUMBERFORMAT_ENTRY_NOT_FOUND) + { + sal_Int32 nCheckPos; + SvNumFormatType nType; + OUString rFormat(rFormatString); + if (!GetOrCreateFormatter()->PutEntry(rFormat, nCheckPos, nType, nNewKey, eLang)) + return false; + DBG_ASSERT(nNewKey != NUMBERFORMAT_ENTRY_NOT_FOUND, "FormattedField::SetFormatString : PutEntry returned an invalid key !"); + } + + if (nNewKey != m_nFormatKey) + SetFormatKey(nNewKey); + return true; +} + +bool Formatter::GetThousandsSep() const +{ + DBG_ASSERT(!GetOrCreateFormatter()->IsTextFormat(m_nFormatKey), + "FormattedField::GetThousandsSep : Are you sure what you are doing when setting the precision of a text format?"); + + bool bThousand, IsRed; + sal_uInt16 nPrecision, nLeadingCnt; + GetOrCreateFormatter()->GetFormatSpecialInfo(m_nFormatKey, bThousand, IsRed, nPrecision, nLeadingCnt); + + return bThousand; +} + +void Formatter::SetThousandsSep(bool _bUseSeparator) +{ + DBG_ASSERT(!GetOrCreateFormatter()->IsTextFormat(m_nFormatKey), + "FormattedField::SetThousandsSep : Are you sure what you are doing when setting the precision of a text format?"); + + // get the current settings + bool bThousand, IsRed; + sal_uInt16 nPrecision, nLeadingCnt; + GetOrCreateFormatter()->GetFormatSpecialInfo(m_nFormatKey, bThousand, IsRed, nPrecision, nLeadingCnt); + if (bThousand == _bUseSeparator) + return; + + // we need the language for the following + LanguageType eLang; + GetFormat(eLang); + + // generate a new format ... + OUString sFmtDescription = GetOrCreateFormatter()->GenerateFormat(m_nFormatKey, eLang, _bUseSeparator, IsRed, nPrecision, nLeadingCnt); + // ... and introduce it to the formatter + sal_Int32 nCheckPos = 0; + sal_uInt32 nNewKey; + SvNumFormatType nType; + GetOrCreateFormatter()->PutEntry(sFmtDescription, nCheckPos, nType, nNewKey, eLang); + + // set the new key + ImplSetFormatKey(nNewKey); + FormatChanged(FORMAT_CHANGE_TYPE::THOUSANDSSEP); +} + +sal_uInt16 Formatter::GetDecimalDigits() const +{ + DBG_ASSERT(!GetOrCreateFormatter()->IsTextFormat(m_nFormatKey), + "FormattedField::GetDecimalDigits : Are you sure what you are doing when setting the precision of a text format?"); + + bool bThousand, IsRed; + sal_uInt16 nPrecision, nLeadingCnt; + GetOrCreateFormatter()->GetFormatSpecialInfo(m_nFormatKey, bThousand, IsRed, nPrecision, nLeadingCnt); + + return nPrecision; +} + +void Formatter::SetDecimalDigits(sal_uInt16 _nPrecision) +{ + DBG_ASSERT(!GetOrCreateFormatter()->IsTextFormat(m_nFormatKey), + "FormattedField::SetDecimalDigits : Are you sure what you are doing when setting the precision of a text format?"); + + // get the current settings + bool bThousand, IsRed; + sal_uInt16 nPrecision, nLeadingCnt; + GetOrCreateFormatter()->GetFormatSpecialInfo(m_nFormatKey, bThousand, IsRed, nPrecision, nLeadingCnt); + if (nPrecision == _nPrecision) + return; + + // we need the language for the following + LanguageType eLang; + GetFormat(eLang); + + // generate a new format ... + OUString sFmtDescription = GetOrCreateFormatter()->GenerateFormat(m_nFormatKey, eLang, bThousand, IsRed, _nPrecision, nLeadingCnt); + // ... and introduce it to the formatter + sal_Int32 nCheckPos = 0; + sal_uInt32 nNewKey; + SvNumFormatType nType; + GetOrCreateFormatter()->PutEntry(sFmtDescription, nCheckPos, nType, nNewKey, eLang); + + // set the new key + ImplSetFormatKey(nNewKey); + FormatChanged(FORMAT_CHANGE_TYPE::PRECISION); +} + +void Formatter::FormatChanged(FORMAT_CHANGE_TYPE _nWhat) +{ + m_pLastOutputColor = nullptr; + + if ( (_nWhat == FORMAT_CHANGE_TYPE::FORMATTER) && m_pFormatter ) + m_pFormatter->SetEvalDateFormat( NF_EVALDATEFORMAT_FORMAT_INTL ); + + ReFormat(); +} + +void Formatter::EntryLostFocus() +{ + // special treatment for empty texts + if (GetEntryText().isEmpty()) + { + if (!IsEmptyFieldEnabled()) + { + if (TreatingAsNumber()) + { + ImplSetValue(m_dCurrentValue, true); + Modify(); + m_ValueState = valueDouble; + } + else + { + OUString sNew = GetTextValue(); + if (!sNew.isEmpty()) + SetTextFormatted(sNew); + else + SetTextFormatted(m_sDefaultText); + m_ValueState = valueString; + } + } + } + else + { + Commit(); + } +} + +void Formatter::Commit() +{ + // remember the old text + OUString sOld(GetEntryText()); + + // do the reformat + ReFormat(); + + // did the text change? + if (GetEntryText() != sOld) + { // consider the field as modified, + // but we already have the most recent value; + // don't reparse it from the text + // (can lead to data loss when the format is lossy, + // as is e.g. our default date format: 2-digit year!) + Modify(false); + } +} + +void Formatter::ReFormat() +{ + if (!IsEmptyFieldEnabled() || !GetEntryText().isEmpty()) + { + if (TreatingAsNumber()) + { + double dValue = GetValue(); + if ( m_bEnableNaN && std::isnan( dValue ) ) + return; + ImplSetValue( dValue, true ); + } + else + SetTextFormatted(GetTextValue()); + } +} + +void Formatter::SetMinValue(double dMin) +{ + DBG_ASSERT(m_bTreatAsNumber, "FormattedField::SetMinValue : only to be used in numeric mode !"); + + m_dMinValue = dMin; + m_bHasMin = true; + // for checking the current value at the new border -> ImplSetValue + ReFormat(); +} + +void Formatter::SetMaxValue(double dMax) +{ + DBG_ASSERT(m_bTreatAsNumber, "FormattedField::SetMaxValue : only to be used in numeric mode !"); + + m_dMaxValue = dMax; + m_bHasMax = true; + // for checking the current value at the new border -> ImplSetValue + ReFormat(); +} + +void Formatter::SetTextValue(const OUString& rText) +{ + SetFieldText(rText, Selection(0, 0)); + ReFormat(); +} + +void Formatter::EnableEmptyField(bool bEnable) +{ + if (bEnable == m_bEnableEmptyField) + return; + + m_bEnableEmptyField = bEnable; + if (!m_bEnableEmptyField && GetEntryText().isEmpty()) + ImplSetValue(m_dCurrentValue, true); +} + +void Formatter::ImplSetValue(double dVal, bool bForce) +{ + if (m_bHasMin && (dVal<m_dMinValue)) + { + dVal = m_bWrapOnLimits ? fmod(dVal + m_dMaxValue + 1 - m_dMinValue, m_dMaxValue + 1) + m_dMinValue + : m_dMinValue; + } + if (m_bHasMax && (dVal>m_dMaxValue)) + { + dVal = m_bWrapOnLimits ? fmod(dVal - m_dMinValue, m_dMaxValue + 1) + m_dMinValue + : m_dMaxValue; + } + if (!bForce && (dVal == GetValue())) + return; + + DBG_ASSERT(GetOrCreateFormatter() != nullptr, "FormattedField::ImplSetValue : can't set a value without a formatter !"); + + m_ValueState = valueDouble; + UpdateCurrentValue(dVal); + + if (!m_aOutputHdl.IsSet() || !m_aOutputHdl.Call(nullptr)) + { + OUString sNewText; + if (GetOrCreateFormatter()->IsTextFormat(m_nFormatKey)) + { + // first convert the number as string in standard format + OUString sTemp; + GetOrCreateFormatter()->GetOutputString(dVal, 0, sTemp, &m_pLastOutputColor); + // then encode the string in the corresponding text format + GetOrCreateFormatter()->GetOutputString(sTemp, m_nFormatKey, sNewText, &m_pLastOutputColor); + } + else + { + if( IsUsingInputStringForFormatting()) + { + GetOrCreateFormatter()->GetInputLineString(dVal, m_nFormatKey, sNewText); + } + else + { + GetOrCreateFormatter()->GetOutputString(dVal, m_nFormatKey, sNewText, &m_pLastOutputColor); + } + } + ImplSetTextImpl(sNewText, nullptr); + DBG_ASSERT(CheckText(sNewText), "FormattedField::ImplSetValue : formatted string doesn't match the criteria !"); + } + + m_ValueState = valueDouble; +} + +bool Formatter::ImplGetValue(double& dNewVal) +{ + dNewVal = m_dCurrentValue; + if (m_ValueState == valueDouble) + return true; + + dNewVal = m_dDefaultValue; + OUString sText(GetEntryText()); + if (sText.isEmpty()) + return true; + + bool bUseExternalFormatterValue = false; + if (m_aInputHdl.IsSet()) + { + sal_Int64 nResult; + auto eState = m_aInputHdl.Call(&nResult); + bUseExternalFormatterValue = eState != TRISTATE_INDET; + if (bUseExternalFormatterValue) + { + if (eState == TRISTATE_TRUE) + { + dNewVal = nResult; + dNewVal /= weld::SpinButton::Power10(GetDecimalDigits()); + } + else + dNewVal = m_dCurrentValue; + } + } + + if (!bUseExternalFormatterValue) + { + DBG_ASSERT(GetOrCreateFormatter() != nullptr, "FormattedField::ImplGetValue : can't give you a current value without a formatter !"); + + sal_uInt32 nFormatKey = m_nFormatKey; // IsNumberFormat changes the FormatKey! + + if (GetOrCreateFormatter()->IsTextFormat(nFormatKey) && m_bTreatAsNumber) + // for detection of values like "1,1" in fields that are formatted as text + nFormatKey = 0; + + // special treatment for percentage formatting + if (GetOrCreateFormatter()->GetType(m_nFormatKey) == SvNumFormatType::PERCENT) + { + // the language of our format + LanguageType eLanguage = m_pFormatter->GetEntry(m_nFormatKey)->GetLanguage(); + // the default number format for this language + sal_uLong nStandardNumericFormat = m_pFormatter->GetStandardFormat(SvNumFormatType::NUMBER, eLanguage); + + sal_uInt32 nTempFormat = nStandardNumericFormat; + double dTemp; + if (m_pFormatter->IsNumberFormat(sText, nTempFormat, dTemp) && + SvNumFormatType::NUMBER == m_pFormatter->GetType(nTempFormat)) + // the string is equivalent to a number formatted one (has no % sign) -> append it + sText += "%"; + // (with this, an input of '3' becomes '3%', which then by the formatter is translated + // into 0.03. Without this, the formatter would give us the double 3 for an input '3', + // which equals 300 percent. + } + if (!GetOrCreateFormatter()->IsNumberFormat(sText, nFormatKey, dNewVal)) + return false; + } + + if (m_bHasMin && (dNewVal<m_dMinValue)) + dNewVal = m_dMinValue; + if (m_bHasMax && (dNewVal>m_dMaxValue)) + dNewVal = m_dMaxValue; + return true; +} + +void Formatter::SetValue(double dVal) +{ + ImplSetValue(dVal, m_ValueState != valueDouble); +} + +double Formatter::GetValue() +{ + if ( !ImplGetValue( m_dCurrentValue ) ) + UpdateCurrentValue(m_bEnableNaN ? std::numeric_limits<double>::quiet_NaN() : m_dDefaultValue); + + m_ValueState = valueDouble; + return m_dCurrentValue; +} + +void Formatter::DisableRemainderFactor() +{ + m_bDisableRemainderFactor = true; +} + +void Formatter::UseInputStringForFormatting() +{ + m_bUseInputStringForFormatting = true; +} + +namespace +{ + class FieldFormatter : public Formatter + { + private: + FormattedField& m_rSpinButton; + public: + FieldFormatter(FormattedField& rSpinButton) + : m_rSpinButton(rSpinButton) + { + } + + // Formatter overrides + virtual Selection GetEntrySelection() const override + { + return m_rSpinButton.GetSelection(); + } + + virtual OUString GetEntryText() const override + { + return m_rSpinButton.GetText(); + } + + void SetEntryText(const OUString& rText, const Selection& rSel) override + { + m_rSpinButton.SpinField::SetText(rText, rSel); + } + + virtual void SetEntryTextColor(const Color* pColor) override + { + if (pColor) + m_rSpinButton.SetControlForeground(*pColor); + else + m_rSpinButton.SetControlForeground(); + } + + virtual SelectionOptions GetEntrySelectionOptions() const override + { + return m_rSpinButton.GetSettings().GetStyleSettings().GetSelectionOptions(); + } + + virtual void FieldModified() override + { + m_rSpinButton.SpinField::Modify(); + } + + virtual void UpdateCurrentValue(double dCurrentValue) override + { + Formatter::UpdateCurrentValue(dCurrentValue); + m_rSpinButton.SetUpperEnabled(!m_bHasMax || dCurrentValue < m_dMaxValue); + m_rSpinButton.SetLowerEnabled(!m_bHasMin || dCurrentValue > m_dMinValue); + } + }; + + class DoubleNumericFormatter : public FieldFormatter + { + private: + DoubleNumericField& m_rNumericSpinButton; + public: + DoubleNumericFormatter(DoubleNumericField& rNumericSpinButton) + : FieldFormatter(rNumericSpinButton) + , m_rNumericSpinButton(rNumericSpinButton) + { + } + + virtual bool CheckText(const OUString& sText) const override + { + // We'd like to implement this using the NumberFormatter::IsNumberFormat, but unfortunately, this doesn't + // recognize fragments of numbers (like, for instance "1e", which happens during entering e.g. "1e10") + // Thus, the roundabout way via a regular expression + return m_rNumericSpinButton.GetNumberValidator().isValidNumericFragment(sText); + } + + virtual void FormatChanged(FORMAT_CHANGE_TYPE nWhat) override + { + m_rNumericSpinButton.ResetConformanceTester(); + FieldFormatter::FormatChanged(nWhat); + } + }; + + class DoubleCurrencyFormatter : public FieldFormatter + { + private: + DoubleCurrencyField& m_rCurrencySpinButton; + bool m_bChangingFormat; + public: + DoubleCurrencyFormatter(DoubleCurrencyField& rNumericSpinButton) + : FieldFormatter(rNumericSpinButton) + , m_rCurrencySpinButton(rNumericSpinButton) + , m_bChangingFormat(false) + { + } + + virtual void FormatChanged(FORMAT_CHANGE_TYPE nWhat) override + { + if (m_bChangingFormat) + { + FieldFormatter::FormatChanged(nWhat); + return; + } + + switch (nWhat) + { + case FORMAT_CHANGE_TYPE::FORMATTER: + case FORMAT_CHANGE_TYPE::PRECISION: + case FORMAT_CHANGE_TYPE::THOUSANDSSEP: + // the aspects which changed don't take our currency settings into account (in fact, they most probably + // destroyed them) + m_rCurrencySpinButton.UpdateCurrencyFormat(); + break; + case FORMAT_CHANGE_TYPE::KEYONLY: + OSL_FAIL("DoubleCurrencyField::FormatChanged : somebody modified my key !"); + // We always build our own format from the settings we get via special methods (setCurrencySymbol etc.). + // Nobody but ourself should modify the format key directly! + break; + default: break; + } + + FieldFormatter::FormatChanged(nWhat); + } + + void GuardSetFormat(const OUString& rString, LanguageType eLanguage) + { + // set this new basic format + m_bChangingFormat = true; + SetFormat(rString, eLanguage); + m_bChangingFormat = false; + } + + }; +} + +DoubleNumericField::DoubleNumericField(vcl::Window* pParent, WinBits nStyle) + : FormattedField(pParent, nStyle) +{ + m_xOwnFormatter.reset(new DoubleNumericFormatter(*this)); + m_pFormatter = m_xOwnFormatter.get(); + ResetConformanceTester(); +} + +DoubleNumericField::~DoubleNumericField() = default; + +void DoubleNumericField::ResetConformanceTester() +{ + // the thousands and the decimal separator are language dependent + Formatter& rFormatter = GetFormatter(); + const SvNumberformat* pFormatEntry = rFormatter.GetOrCreateFormatter()->GetEntry(rFormatter.GetFormatKey()); + + sal_Unicode cSeparatorThousand = ','; + sal_Unicode cSeparatorDecimal = '.'; + if (pFormatEntry) + { + LocaleDataWrapper aLocaleInfo( LanguageTag( pFormatEntry->GetLanguage()) ); + + OUString sSeparator = aLocaleInfo.getNumThousandSep(); + if (!sSeparator.isEmpty()) + cSeparatorThousand = sSeparator[0]; + + sSeparator = aLocaleInfo.getNumDecimalSep(); + if (!sSeparator.isEmpty()) + cSeparatorDecimal = sSeparator[0]; + } + + m_pNumberValidator.reset(new validation::NumberValidator( cSeparatorThousand, cSeparatorDecimal )); +} + + +DoubleCurrencyField::DoubleCurrencyField(vcl::Window* pParent, WinBits nStyle) + :FormattedField(pParent, nStyle) +{ + m_xOwnFormatter.reset(new DoubleCurrencyFormatter(*this)); + m_pFormatter = m_xOwnFormatter.get(); + + m_bPrependCurrSym = false; + + // initialize with a system currency format + m_sCurrencySymbol = SvtSysLocale().GetLocaleData().getCurrSymbol(); + UpdateCurrencyFormat(); +} + +void DoubleCurrencyField::setCurrencySymbol(const OUString& rSymbol) +{ + if (m_sCurrencySymbol == rSymbol) + return; + + m_sCurrencySymbol = rSymbol; + UpdateCurrencyFormat(); + m_pFormatter->FormatChanged(FORMAT_CHANGE_TYPE::CURRENCY_SYMBOL); +} + +void DoubleCurrencyField::setPrependCurrSym(bool _bPrepend) +{ + if (m_bPrependCurrSym == _bPrepend) + return; + + m_bPrependCurrSym = _bPrepend; + UpdateCurrencyFormat(); + m_pFormatter->FormatChanged(FORMAT_CHANGE_TYPE::CURRSYM_POSITION); +} + +void DoubleCurrencyField::UpdateCurrencyFormat() +{ + // the old settings + LanguageType eLanguage; + m_pFormatter->GetFormat(eLanguage); + bool bThSep = m_pFormatter->GetThousandsSep(); + sal_uInt16 nDigits = m_pFormatter->GetDecimalDigits(); + + // build a new format string with the base class' and my own settings + + /* Strangely with gcc 4.6.3 this needs a temporary LanguageTag, otherwise + * there's + * error: request for member 'getNumThousandSep' in 'aLocaleInfo', which is + * of non-class type 'LocaleDataWrapper(LanguageTag)' */ + LocaleDataWrapper aLocaleInfo(( LanguageTag(eLanguage) )); + + OUStringBuffer sNewFormat; + if (bThSep) + { + sNewFormat.append('#'); + sNewFormat.append(aLocaleInfo.getNumThousandSep()); + sNewFormat.append("##0"); + } + else + sNewFormat.append('0'); + + if (nDigits) + { + sNewFormat.append(aLocaleInfo.getNumDecimalSep()); + comphelper::string::padToLength(sNewFormat, sNewFormat.getLength() + nDigits, '0'); + } + + if (getPrependCurrSym()) + { + OUString sSymbol = getCurrencySymbol(); + sSymbol = comphelper::string::strip(sSymbol, ' '); + + OUStringBuffer sTemp("[$"); + sTemp.append(sSymbol); + sTemp.append("] "); + sTemp.append(sNewFormat); + + // for negative values : $ -0.00, not -$ 0.00... + // (the real solution would be a possibility to choose a "positive currency format" and a "negative currency format"... + // But not now... (and hey, you could take a formatted field for this...)) + // FS - 31.03.00 74642 + sTemp.append(";[$"); + sTemp.append(sSymbol); + sTemp.append("] -"); + sTemp.append(sNewFormat); + + sNewFormat = sTemp; + } + else + { + OUString sTemp = getCurrencySymbol(); + sTemp = comphelper::string::strip(sTemp, ' '); + + sNewFormat.append(" [$"); + sNewFormat.append(sTemp); + sNewFormat.append(']'); + } + + // set this new basic format + static_cast<DoubleCurrencyFormatter*>(m_pFormatter)->GuardSetFormat(sNewFormat.makeStringAndClear(), eLanguage); +} + +FormattedField::FormattedField(vcl::Window* pParent, WinBits nStyle) + : SpinField(pParent, nStyle, WindowType::FORMATTEDFIELD) + , m_pFormatter(nullptr) +{ +} + +void FormattedField::dispose() +{ + m_pFormatter = nullptr; + m_xOwnFormatter.reset(); + SpinField::dispose(); +} + +void FormattedField::SetText(const OUString& rStr) +{ + GetFormatter().SetFieldText(rStr, Selection(0, 0)); +} + +void FormattedField::SetText(const OUString& rStr, const Selection& rNewSelection) +{ + GetFormatter().SetFieldText(rStr, rNewSelection); + SetSelection(rNewSelection); +} + +bool FormattedField::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "digits") + GetFormatter().SetDecimalDigits(rValue.toInt32()); + else if (rKey == "wrap") + GetFormatter().SetWrapOnLimits(toBool(rValue)); + else + return SpinField::set_property(rKey, rValue); + return true; +} + +void FormattedField::Up() +{ + Formatter& rFormatter = GetFormatter(); + auto nScale = weld::SpinButton::Power10(rFormatter.GetDecimalDigits()); + + sal_Int64 nValue = std::round(rFormatter.GetValue() * nScale); + sal_Int64 nSpinSize = std::round(rFormatter.GetSpinSize() * nScale); + assert(nSpinSize != 0); + sal_Int64 nRemainder = rFormatter.GetDisableRemainderFactor() || nSpinSize == 0 ? 0 : nValue % nSpinSize; + if (nValue >= 0) + nValue = (nRemainder == 0) ? nValue + nSpinSize : nValue + nSpinSize - nRemainder; + else + nValue = (nRemainder == 0) ? nValue + nSpinSize : nValue - nRemainder; + + // setValue handles under- and overflows (min/max) automatically + rFormatter.SetValue(static_cast<double>(nValue) / nScale); + SetModifyFlag(); + Modify(); + + SpinField::Up(); +} + +void FormattedField::Down() +{ + Formatter& rFormatter = GetFormatter(); + auto nScale = weld::SpinButton::Power10(rFormatter.GetDecimalDigits()); + + sal_Int64 nValue = std::round(rFormatter.GetValue() * nScale); + sal_Int64 nSpinSize = std::round(rFormatter.GetSpinSize() * nScale); + assert(nSpinSize != 0); + sal_Int64 nRemainder = rFormatter.GetDisableRemainderFactor() || nSpinSize == 0 ? 0 : nValue % nSpinSize; + if (nValue >= 0) + nValue = (nRemainder == 0) ? nValue - nSpinSize : nValue - nRemainder; + else + nValue = (nRemainder == 0) ? nValue - nSpinSize : nValue - nSpinSize - nRemainder; + + // setValue handles under- and overflows (min/max) automatically + rFormatter.SetValue(static_cast<double>(nValue) / nScale); + SetModifyFlag(); + Modify(); + + SpinField::Down(); +} + +void FormattedField::First() +{ + Formatter& rFormatter = GetFormatter(); + if (rFormatter.HasMinValue()) + { + rFormatter.SetValue(rFormatter.GetMinValue()); + SetModifyFlag(); + Modify(); + } + + SpinField::First(); +} + +void FormattedField::Last() +{ + Formatter& rFormatter = GetFormatter(); + if (rFormatter.HasMaxValue()) + { + rFormatter.SetValue(rFormatter.GetMaxValue()); + SetModifyFlag(); + Modify(); + } + + SpinField::Last(); +} + +void FormattedField::Modify() +{ + GetFormatter().Modify(); +} + +bool FormattedField::PreNotify(NotifyEvent& rNEvt) +{ + if (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) + GetFormatter().SetLastSelection(GetSelection()); + return SpinField::PreNotify(rNEvt); +} + +bool FormattedField::EventNotify(NotifyEvent& rNEvt) +{ + if ((rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && !IsReadOnly()) + { + const KeyEvent& rKEvt = *rNEvt.GetKeyEvent(); + sal_uInt16 nMod = rKEvt.GetKeyCode().GetModifier(); + switch ( rKEvt.GetKeyCode().GetCode() ) + { + case KEY_UP: + case KEY_DOWN: + case KEY_PAGEUP: + case KEY_PAGEDOWN: + { + Formatter& rFormatter = GetFormatter(); + if (!nMod && rFormatter.GetOrCreateFormatter()->IsTextFormat(rFormatter.GetFormatKey())) + { + // the base class would translate this into calls to Up/Down/First/Last, + // but we don't want this if we are text-formatted + return true; + } + } + } + } + + if ((rNEvt.GetType() == MouseNotifyEvent::COMMAND) && !IsReadOnly()) + { + const CommandEvent* pCommand = rNEvt.GetCommandEvent(); + if (pCommand->GetCommand() == CommandEventId::Wheel) + { + const CommandWheelData* pData = rNEvt.GetCommandEvent()->GetWheelData(); + Formatter& rFormatter = GetFormatter(); + if ((pData->GetMode() == CommandWheelMode::SCROLL) && + rFormatter.GetOrCreateFormatter()->IsTextFormat(rFormatter.GetFormatKey())) + { + // same as above : prevent the base class from doing Up/Down-calls + // (normally I should put this test into the Up/Down methods itself, shouldn't I ?) + // FS - 71553 - 19.01.00 + return true; + } + } + } + + if (rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS && m_pFormatter) + m_pFormatter->EntryLostFocus(); + + return SpinField::EventNotify( rNEvt ); +} + +Formatter& FormattedField::GetFormatter() +{ + if (!m_pFormatter) + { + m_xOwnFormatter.reset(new FieldFormatter(*this)); + m_pFormatter = m_xOwnFormatter.get(); + } + return *m_pFormatter; +} + +void FormattedField::SetFormatter(Formatter* pFormatter) +{ + m_xOwnFormatter.reset(); + m_pFormatter = pFormatter; +} + +// currently used by online +void FormattedField::SetValueFromString(const OUString& rStr) +{ + sal_Int32 nEnd; + rtl_math_ConversionStatus eStatus; + Formatter& rFormatter = GetFormatter(); + double fValue = ::rtl::math::stringToDouble(rStr, '.', rFormatter.GetDecimalDigits(), &eStatus, &nEnd ); + + if (eStatus == rtl_math_ConversionStatus_Ok && + nEnd == rStr.getLength()) + { + rFormatter.SetValue(fValue); + SetModifyFlag(); + Modify(); + + // Notify the value has changed + SpinField::Up(); + } + else + { + SAL_WARN("vcl", "fail to convert the value: " << rStr); + } +} + +void FormattedField::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + SpinField::DumpAsPropertyTree(rJsonWriter); + Formatter& rFormatter = GetFormatter(); + rJsonWriter.put("min", rFormatter.GetMinValue()); + rJsonWriter.put("max", rFormatter.GetMaxValue()); + rJsonWriter.put("value", rFormatter.GetValue()); + rJsonWriter.put("step", rFormatter.GetSpinSize()); +} + +FactoryFunction FormattedField::GetUITestFactory() const +{ + return FormattedFieldUIObject::create; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/hyperlabel.cxx b/vcl/source/control/hyperlabel.cxx new file mode 100644 index 000000000..34f10750a --- /dev/null +++ b/vcl/source/control/hyperlabel.cxx @@ -0,0 +1,176 @@ +/* -*- 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 <tools/color.hxx> +#include <vcl/event.hxx> +#include <vcl/settings.hxx> +#include <vcl/ptrstyle.hxx> +#include <hyperlabel.hxx> + +namespace vcl +{ + HyperLabel::HyperLabel( vcl::Window* _pParent, WinBits _nWinStyle ) + :FixedText( _pParent, _nWinStyle ) + , ID(0) + , Index(0) + , bInteractive(false) + , m_bHyperMode(false) + { + implInit(); + } + + Size const & HyperLabel::CalcMinimumSize( tools::Long nMaxWidth ) + { + m_aMinSize = FixedText::CalcMinimumSize( nMaxWidth ); + // the MinimumSize is used to size the FocusRectangle + // and for the MouseMove method + m_aMinSize.AdjustHeight(2 ); + m_aMinSize.AdjustWidth(1 ); + return m_aMinSize; + } + + void HyperLabel::implInit() + { + ToggleBackgroundColor( COL_TRANSPARENT ); + + WinBits nWinStyle = GetStyle(); + nWinStyle |= WB_EXTRAOFFSET; + SetStyle( nWinStyle ); + + Show(); + } + + void HyperLabel::ToggleBackgroundColor( const Color& _rGBColor ) + { + SetControlBackground( _rGBColor ); + } + + void HyperLabel::MouseMove( const MouseEvent& rMEvt ) + { + vcl::Font aFont = GetControlFont( ); + + bool bHyperMode = false; + if (!rMEvt.IsLeaveWindow() && IsEnabled() && bInteractive) + { + Point aPoint = GetPointerPosPixel(); + if (aPoint.X() < m_aMinSize.Width()) + bHyperMode = true; + } + + m_bHyperMode = bHyperMode; + if (bHyperMode) + { + aFont.SetUnderline(LINESTYLE_SINGLE); + SetPointer(PointerStyle::RefHand); + } + else + { + aFont.SetUnderline(LINESTYLE_NONE); + SetPointer(PointerStyle::Arrow); + } + SetControlFont(aFont); + } + + void HyperLabel::MouseButtonDown( const MouseEvent& ) + { + if ( m_bHyperMode && bInteractive ) + { + maClickHdl.Call( this ); + } + } + + void HyperLabel::GetFocus() + { + if ( IsEnabled() && bInteractive ) + { + Point aPoint(0,0); + tools::Rectangle rRect(aPoint, Size( m_aMinSize.Width(), GetSizePixel().Height() ) ); + ShowFocus( rRect ); + } + } + + void HyperLabel::LoseFocus() + { + HideFocus(); + } + + HyperLabel::~HyperLabel( ) + { + disposeOnce(); + } + + void HyperLabel::SetInteractive( bool _bInteractive ) + { + bInteractive = ( _bInteractive && IsEnabled() ); + } + + sal_Int16 HyperLabel::GetID() const + { + return ID; + } + + sal_Int32 HyperLabel::GetIndex() const + { + return Index; + } + + void HyperLabel::SetID( sal_Int16 newID ) + { + this->ID = newID; + } + + void HyperLabel::SetIndex( sal_Int32 newIndex ) + { + Index = newIndex; + } + + void HyperLabel::SetLabel( const OUString& _rText ) + { + SetText(_rText); + } + + void HyperLabel::ApplySettings(vcl::RenderContext& rRenderContext) + { + FixedText::ApplySettings(rRenderContext); + + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + if (GetControlBackground() == COL_TRANSPARENT) + rRenderContext.SetTextColor(rStyleSettings.GetFieldTextColor()); + else + rRenderContext.SetTextColor(rStyleSettings.GetHighlightTextColor()); + } + + void HyperLabel::DataChanged( const DataChangedEvent& rDCEvt ) + { + FixedText::DataChanged( rDCEvt ); + + if ((( rDCEvt.GetType() == DataChangedEventType::SETTINGS ) || + ( rDCEvt.GetType() == DataChangedEventType::DISPLAY )) && + ( rDCEvt.GetFlags() & AllSettingsFlags::STYLE )) + { + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + if (GetControlBackground() != COL_TRANSPARENT) + SetControlBackground(rStyleSettings.GetHighlightColor()); + Invalidate(); + } + } + +} // namespace vcl + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/imgctrl.cxx b/vcl/source/control/imgctrl.cxx new file mode 100644 index 000000000..414824b29 --- /dev/null +++ b/vcl/source/control/imgctrl.cxx @@ -0,0 +1,184 @@ +/* -*- 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 <vcl/toolkit/imgctrl.hxx> + +#include <com/sun/star/awt/ImageScaleMode.hpp> +#include <osl/diagnose.h> + +namespace ImageScaleMode = css::awt::ImageScaleMode; + +ImageControl::ImageControl( vcl::Window* pParent, WinBits nStyle ) + :FixedImage( pParent, nStyle ) + ,mnScaleMode( ImageScaleMode::ANISOTROPIC ) +{ +} + +void ImageControl::SetScaleMode( const ::sal_Int16 _nMode ) +{ + if ( _nMode != mnScaleMode ) + { + mnScaleMode = _nMode; + Invalidate(); + } +} + +void ImageControl::Resize() +{ + Invalidate(); +} + +namespace +{ + Size lcl_calcPaintSize( const tools::Rectangle& _rPaintRect, const Size& _rBitmapSize ) + { + const Size aPaintSize = _rPaintRect.GetSize(); + + const double nRatioX = 1.0 * aPaintSize.Width() / _rBitmapSize.Width(); + const double nRatioY = 1.0 * aPaintSize.Height() / _rBitmapSize.Height(); + const double nRatioMin = ::std::min( nRatioX, nRatioY ); + + return Size( tools::Long( _rBitmapSize.Width() * nRatioMin ), tools::Long( _rBitmapSize.Height() * nRatioMin ) ); + } + + Point lcl_centerWithin( const tools::Rectangle& _rArea, const Size& _rObjectSize ) + { + Point aPos( _rArea.TopLeft() ); + aPos.AdjustX(( _rArea.GetWidth() - _rObjectSize.Width() ) / 2 ); + aPos.AdjustY(( _rArea.GetHeight() - _rObjectSize.Height() ) / 2 ); + return aPos; + } +} + +void ImageControl::ImplDraw(OutputDevice& rDev, const Point& rPos, const Size& rSize) const +{ + DrawImageFlags nStyle = DrawImageFlags::NONE; + if ( !IsEnabled() ) + nStyle |= DrawImageFlags::Disable; + + const Image& rImage( GetModeImage() ); + const tools::Rectangle aDrawRect( rPos, rSize ); + if (!rImage) + { + OUString sText( GetText() ); + if ( sText.isEmpty() ) + return; + + WinBits nWinStyle = GetStyle(); + DrawTextFlags nTextStyle = FixedText::ImplGetTextStyle( nWinStyle ); + if ( !IsEnabled() ) + nTextStyle |= DrawTextFlags::Disable; + + rDev.DrawText( aDrawRect, sText, nTextStyle ); + return; + } + + const Size& rBitmapSize = rImage.GetSizePixel(); + + switch ( mnScaleMode ) + { + case ImageScaleMode::NONE: + { + rDev.DrawImage(lcl_centerWithin( aDrawRect, rBitmapSize ), rImage, nStyle); + } + break; + + case ImageScaleMode::ISOTROPIC: + { + const Size aPaintSize = lcl_calcPaintSize( aDrawRect, rBitmapSize ); + rDev.DrawImage(lcl_centerWithin(aDrawRect, aPaintSize), aPaintSize, rImage, nStyle); + } + break; + + case ImageScaleMode::ANISOTROPIC: + { + rDev.DrawImage( + aDrawRect.TopLeft(), + aDrawRect.GetSize(), + rImage, nStyle ); + } + break; + + default: + OSL_ENSURE( false, "ImageControl::ImplDraw: unhandled scale mode!" ); + break; + + } // switch ( mnScaleMode ) +} + +void ImageControl::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& /*rRect*/) +{ + ImplDraw(rRenderContext, Point(), GetOutputSizePixel()); + + if (!HasFocus()) + return; + + vcl::Window* pBorderWindow = GetWindow(GetWindowType::Border); + + bool bFlat = (GetBorderStyle() == WindowBorderStyle::MONO); + tools::Rectangle aRect(Point(0,0), pBorderWindow->GetOutputSizePixel()); + Color oldLineCol = pBorderWindow->GetOutDev()->GetLineColor(); + Color oldFillCol = pBorderWindow->GetOutDev()->GetFillColor(); + pBorderWindow->GetOutDev()->SetFillColor(); + pBorderWindow->GetOutDev()->SetLineColor(bFlat ? COL_WHITE : COL_BLACK); + pBorderWindow->GetOutDev()->DrawRect(aRect); + aRect.AdjustLeft( 1 ); + aRect.AdjustRight( -1 ); + aRect.AdjustTop( 1 ); + aRect.AdjustBottom( -1 ); + pBorderWindow->GetOutDev()->SetLineColor(bFlat ? COL_BLACK : COL_WHITE); + pBorderWindow->GetOutDev()->DrawRect(aRect); + pBorderWindow->GetOutDev()->SetLineColor(oldLineCol); + pBorderWindow->GetOutDev()->SetFillColor(oldFillCol); + +} + +void ImageControl::Draw( OutputDevice* pDev, const Point& rPos, SystemTextColorFlags ) +{ + const Point aPos = pDev->LogicToPixel( rPos ); + const Size aSize = GetSizePixel(); + tools::Rectangle aRect( aPos, aSize ); + + pDev->Push(); + pDev->SetMapMode(); + + // Border + if ( GetStyle() & WB_BORDER ) + { + ImplDrawFrame( pDev, aRect ); + } + pDev->IntersectClipRegion( aRect ); + ImplDraw( *pDev, aRect.TopLeft(), aRect.GetSize() ); + + pDev->Pop(); +} + +void ImageControl::GetFocus() +{ + FixedImage::GetFocus(); + GetWindow( GetWindowType::Border )->Invalidate(); +} + +void ImageControl::LoseFocus() +{ + FixedImage::GetFocus(); + GetWindow( GetWindowType::Border )->Invalidate(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/imivctl.hxx b/vcl/source/control/imivctl.hxx new file mode 100644 index 000000000..b242ef91d --- /dev/null +++ b/vcl/source/control/imivctl.hxx @@ -0,0 +1,513 @@ +/* -*- 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 . + */ + +#ifndef INCLUDED_SVTOOLS_SOURCE_CONTNR_IMIVCTL_HXX +#define INCLUDED_SVTOOLS_SOURCE_CONTNR_IMIVCTL_HXX + +#include <sal/config.h> + +#include <o3tl/safeint.hxx> +#include <vcl/toolkit/ivctrl.hxx> +#include <vcl/virdev.hxx> +#include <vcl/scrbar.hxx> +#include <vcl/timer.hxx> +#include <vcl/idle.hxx> +#include <vcl/vclptr.hxx> +#include <tools/debug.hxx> +#include <vcl/svtaccessiblefactory.hxx> + +#include <limits.h> + + +#include <memory> +#include <map> + +class IcnCursor_Impl; +class SvtIconChoiceCtrl; +class SvxIconChoiceCtrlEntry; +class IcnViewEdit_Impl; +class IcnGridMap_Impl; + + +// some defines + +#define PAINTFLAG_HOR_CENTERED 0x0001 +#define PAINTFLAG_VER_CENTERED 0x0002 + +enum class IconChoiceFlags { + NONE = 0x0000, + AddMode = 0x0001, + SelectingRect = 0x0002, + DownCtrl = 0x0004, + DownDeselect = 0x0008, + EntryListPosValid = 0x0010, + ClearingSelection = 0x0020, + Arranging = 0x0040 +}; +namespace o3tl { + template<> struct typed_flags<IconChoiceFlags> : is_typed_flags<IconChoiceFlags, 0x007f> {}; +} + +// unit = pixels +// distances from window borders +#define LROFFS_WINBORDER 4 +#define TBOFFS_WINBORDER 4 +// for the bounding rectangle +#define LROFFS_BOUND 2 +#define TBOFFS_BOUND 2 +// distance icon to text +#define HOR_DIST_BMP_STRING 3 +#define VER_DIST_BMP_STRING 3 +// width offset of highlight rectangle for Text +#define LROFFS_TEXT 2 + +#define DEFAULT_MAX_VIRT_WIDTH 200 +#define DEFAULT_MAX_VIRT_HEIGHT 200 + +#define VIEWMODE_MASK (WB_ICON | WB_SMALLICON | WB_DETAILS) + + +enum class IcnViewFieldType +{ + Image, + Text +}; + + +// Data about the focus of entries + +struct LocalFocus +{ + tools::Rectangle aRect; + Color aPenColor; +}; + + +typedef sal_uLong GridId; + +#define GRID_NOT_FOUND (GridId(ULONG_MAX)) + +// Implementation-class of IconChoiceCtrl + + +typedef std::map<sal_uInt16, std::unique_ptr<SvxIconChoiceCtrlColumnInfo>> SvxIconChoiceCtrlColumnInfoMap; +typedef std::vector<SvxIconChoiceCtrlEntry*> SvxIconChoiceCtrlEntryPtrVec; + +class SvxIconChoiceCtrl_Impl +{ + friend class IcnCursor_Impl; + friend class IcnGridMap_Impl; + + std::vector< std::unique_ptr<SvxIconChoiceCtrlEntry> > maEntries; + VclPtr<ScrollBar> aVerSBar; + VclPtr<ScrollBar> aHorSBar; + VclPtr<ScrollBarBox> aScrBarBox; + tools::Rectangle aCurSelectionRect; + std::vector<tools::Rectangle> aSelectedRectList; + Idle aAutoArrangeIdle; + Idle aDocRectChangedIdle; + Idle aVisRectChangedIdle; + Idle aCallSelectHdlIdle; + Size aVirtOutputSize; + Size aImageSize; + Size aDefaultTextSize; + Size aOutputSize; // Pixel + VclPtr<SvtIconChoiceCtrl> pView; + std::unique_ptr<IcnCursor_Impl> pImpCursor; + std::unique_ptr<IcnGridMap_Impl> pGridMap; + tools::Long nMaxVirtWidth; // max. width aVirtOutputSize for ALIGN_TOP + tools::Long nMaxVirtHeight; // max. height aVirtOutputSize for ALIGN_LEFT + std::vector< SvxIconChoiceCtrlEntry* > maZOrderList; + std::unique_ptr<SvxIconChoiceCtrlColumnInfoMap> m_pColumns; + WinBits nWinBits; + tools::Long nMaxBoundHeight; // height of highest BoundRects + IconChoiceFlags nFlags; + DrawTextFlags nCurTextDrawFlags; + ImplSVEvent * nUserEventAdjustScrBars; + SvxIconChoiceCtrlEntry* pCurHighlightFrame; + bool bHighlightFramePressed; + SvxIconChoiceCtrlEntry* pHead = nullptr; // top left entry + SvxIconChoiceCtrlEntry* pCursor; + SvxIconChoiceCtrlEntry* pHdlEntry; + SvxIconChoiceCtrlEntry* pAnchor; // for selection + LocalFocus aFocus; // Data for focusrect + ::vcl::AccessibleFactoryAccess aAccFactory; + + SvxIconChoiceCtrlTextMode eTextMode; + SelectionMode eSelectionMode; + sal_Int32 nSelectionCount; + SvxIconChoiceCtrlPositionMode ePositionMode; + bool bBoundRectsDirty; + bool bUpdateMode; + + void ShowCursor( bool bShow ); + + void ImpArrange( bool bKeepPredecessors ); + void AdjustVirtSize( const tools::Rectangle& ); + void ResetVirtSize(); + void CheckScrollBars(); + + DECL_LINK( ScrollUpDownHdl, ScrollBar*, void ); + DECL_LINK( ScrollLeftRightHdl, ScrollBar*, void ); + DECL_LINK( UserEventHdl, void*, void ); + DECL_LINK( AutoArrangeHdl, Timer*, void ); + DECL_LINK( DocRectChangedHdl, Timer*, void ); + DECL_LINK( VisRectChangedHdl, Timer*, void ); + DECL_LINK( CallSelectHdlHdl, Timer*, void ); + + void AdjustScrollBars(); + void PositionScrollBars( tools::Long nRealWidth, tools::Long nRealHeight ); + static tools::Long GetScrollBarPageSize( tools::Long nVisibleRange ) + { + return ((nVisibleRange*75)/100); + } + tools::Long GetScrollBarLineSize() const + { + return nMaxBoundHeight / 2; + } + bool HandleScrollCommand( const CommandEvent& rCmd ); + void ToDocPos( Point& rPosPixel ) + { + rPosPixel -= pView->GetMapMode().GetOrigin(); + } + void InitScrollBarBox(); + void ToggleSelection( SvxIconChoiceCtrlEntry* ); + void DeselectAllBut( SvxIconChoiceCtrlEntry const * ); + void Center( SvxIconChoiceCtrlEntry* pEntry ) const; + void CallSelectHandler(); + void SelectRect( + SvxIconChoiceCtrlEntry* pEntry1, + SvxIconChoiceCtrlEntry* pEntry2, + bool bAdd, + std::vector<tools::Rectangle>* pOtherRects + ); + + void SelectRange( + SvxIconChoiceCtrlEntry const * pStart, + SvxIconChoiceCtrlEntry const * pEnd, + bool bAdd + ); + + void AddSelectedRect( const tools::Rectangle& ); + void AddSelectedRect( + SvxIconChoiceCtrlEntry* pEntry1, + SvxIconChoiceCtrlEntry* pEntry2 + ); + + void ClearSelectedRectList(); + tools::Rectangle CalcMaxTextRect( const SvxIconChoiceCtrlEntry* pEntry ) const; + + void ClipAtVirtOutRect( tools::Rectangle& rRect ) const; + GridId GetPredecessorGrid( const Point& rDocPos) const; + + void InitPredecessors(); + void ClearPredecessors(); + + bool CheckVerScrollBar(); + bool CheckHorScrollBar(); + void CancelUserEvents(); + void EntrySelected( + SvxIconChoiceCtrlEntry* pEntry, + bool bSelect + ); + void RepaintSelectedEntries(); + void SetListPositions(); + void SetDefaultTextSize(); + bool IsAutoArrange() const + { + return (ePositionMode == SvxIconChoiceCtrlPositionMode::AutoArrange); + } + void DocRectChanged() { aDocRectChangedIdle.Start(); } + void VisRectChanged() { aVisRectChangedIdle.Start(); } + void SetOrigin( const Point& ); + + void ShowFocus ( tools::Rectangle const & rRect ); + void DrawFocusRect(vcl::RenderContext& rRenderContext); + + bool IsMnemonicChar( sal_Unicode cChar, sal_uLong& rPos ) const; + + // Copy assignment is forbidden and not implemented. + SvxIconChoiceCtrl_Impl (const SvxIconChoiceCtrl_Impl &) = delete; + SvxIconChoiceCtrl_Impl & operator= (const SvxIconChoiceCtrl_Impl &) = delete; + +public: + + tools::Long nGridDX; + tools::Long nGridDY; + tools::Long nHorSBarHeight; + tools::Long nVerSBarWidth; + + SvxIconChoiceCtrl_Impl( SvtIconChoiceCtrl* pView, WinBits nWinStyle ); + ~SvxIconChoiceCtrl_Impl(); + + void SetSelectionMode(SelectionMode eMode) + { + eSelectionMode = eMode; + } + + void Clear( bool bInCtor ); + void SetStyle( WinBits nWinStyle ); + WinBits GetStyle() const { return nWinBits; } + void InsertEntry( std::unique_ptr<SvxIconChoiceCtrlEntry>, size_t nPos ); + void RemoveEntry( size_t nPos ); + void FontModified(); + void SelectAll(); + void SelectEntry( + SvxIconChoiceCtrlEntry*, + bool bSelect, + bool bAddToSelection = false + ); + void Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect); + bool MouseButtonDown( const MouseEvent& ); + bool MouseButtonUp( const MouseEvent& ); + bool MouseMove( const MouseEvent&); + bool RequestHelp( const HelpEvent& rHEvt ); + void SetCursor_Impl( + SvxIconChoiceCtrlEntry* pOldCursor, + SvxIconChoiceCtrlEntry* pNewCursor, + bool bMod1, + bool bShift + ); + bool KeyInput( const KeyEvent& ); + void Resize(); + void GetFocus(); + void LoseFocus(); + void SetUpdateMode( bool bUpdate ); + bool GetUpdateMode() const { return bUpdateMode; } + void PaintEntry(SvxIconChoiceCtrlEntry*, const Point&, vcl::RenderContext& rRenderContext); + + void SetEntryPos( + SvxIconChoiceCtrlEntry* pEntry, + const Point& rPos + ); + + void InvalidateEntry( SvxIconChoiceCtrlEntry* ); + + void SetNoSelection(); + + SvxIconChoiceCtrlEntry* GetCurEntry() const { return pCursor; } + void SetCursor( SvxIconChoiceCtrlEntry* ); + + SvxIconChoiceCtrlEntry* GetEntry( const Point& rDocPos, bool bHit = false ); + + void MakeEntryVisible( SvxIconChoiceCtrlEntry* pEntry, bool bBound = true ); + + void Arrange( + bool bKeepPredecessors, + tools::Long nSetMaxVirtWidth, + tools::Long nSetMaxVirtHeight + ); + + tools::Rectangle CalcFocusRect( SvxIconChoiceCtrlEntry* ); + tools::Rectangle CalcBmpRect( SvxIconChoiceCtrlEntry*, const Point* pPos = nullptr ); + tools::Rectangle CalcTextRect( + SvxIconChoiceCtrlEntry*, + const Point* pPos = nullptr, + const OUString* pStr = nullptr + ); + + tools::Long CalcBoundingWidth() const; + tools::Long CalcBoundingHeight() const; + Size CalcBoundingSize() const; + void FindBoundingRect( SvxIconChoiceCtrlEntry* pEntry ); + void SetBoundingRect_Impl( + SvxIconChoiceCtrlEntry* pEntry, + const Point& rPos, + const Size& rBoundingSize + ); + // recalculates all invalid BoundRects + void RecalcAllBoundingRectsSmart(); + const tools::Rectangle& GetEntryBoundRect( SvxIconChoiceCtrlEntry* ); + void InvalidateBoundingRect( tools::Rectangle& rRect ) + { + rRect.SetRight(LONG_MAX); + bBoundRectsDirty = true; + } + static bool IsBoundingRectValid( const tools::Rectangle& rRect ) { return ( rRect.Right() != LONG_MAX ); } + + static void PaintEmphasis(const tools::Rectangle& rRect1, bool bSelected, + vcl::RenderContext& rRenderContext ); + + void PaintItem(const tools::Rectangle& rRect, IcnViewFieldType eItem, SvxIconChoiceCtrlEntry* pEntry, + sal_uInt16 nPaintFlags, vcl::RenderContext& rRenderContext); + + // recalculates all BoundingRects if bMustRecalcBoundingRects == true + void CheckBoundingRects() { if (bBoundRectsDirty) RecalcAllBoundingRectsSmart(); } + void Command( const CommandEvent& rCEvt ); + void ToTop( SvxIconChoiceCtrlEntry* ); + + sal_Int32 GetSelectionCount() const; + void SetGrid( const Size& ); + Size GetMinGrid() const; + void Scroll( tools::Long nDeltaX, tools::Long nDeltaY ); + const Size& GetItemSize( IcnViewFieldType ) const; + + void HideDDIcon(); + + static bool IsOver( + std::vector<tools::Rectangle>* pSelectedRectList, + const tools::Rectangle& rEntryBoundRect + ); + + void SelectRect( + const tools::Rectangle&, + bool bAdd, + std::vector<tools::Rectangle>* pOtherRects + ); + + void MakeVisible( + const tools::Rectangle& rDocPos, + bool bInScrollBarEvent=false + ); + +#ifdef DBG_UTIL + void SetEntryTextMode( + SvxIconChoiceCtrlTextMode, + SvxIconChoiceCtrlEntry* pEntry + ); +#endif + size_t GetEntryCount() const { return maEntries.size(); } + SvxIconChoiceCtrlEntry* GetEntry( size_t nPos ) + { + return maEntries[ nPos ].get(); + } + SvxIconChoiceCtrlEntry* GetEntry( size_t nPos ) const + { + return maEntries[ nPos ].get(); + } + SvxIconChoiceCtrlEntry* GetFirstSelectedEntry() const; + sal_Int32 GetEntryListPos( SvxIconChoiceCtrlEntry const * ) const; + void InitSettings(); + tools::Rectangle GetOutputRect() const; + + void SetEntryPredecessor(SvxIconChoiceCtrlEntry* pEntry,SvxIconChoiceCtrlEntry* pPredecessor); + // only delivers valid results when in AutoArrange mode! + SvxIconChoiceCtrlEntry* FindEntryPredecessor( SvxIconChoiceCtrlEntry* pEntry, const Point& ); + + void SetPositionMode( SvxIconChoiceCtrlPositionMode ); + + void SetColumn( sal_uInt16 nIndex, const SvxIconChoiceCtrlColumnInfo& ); + const SvxIconChoiceCtrlColumnInfo* GetColumn( sal_uInt16 nIndex ) const; + + void SetEntryHighlightFrame( + SvxIconChoiceCtrlEntry* pEntry, + bool bKeepHighlightFlags + ); + void DrawHighlightFrame(vcl::RenderContext& rRenderContext, const tools::Rectangle& rBmpRect); + + void CallEventListeners( VclEventId nEvent, void* pData ); + + ::vcl::IAccessibleFactory& GetAccessibleFactory() + { + return aAccFactory.getFactory(); + } +}; + +typedef std::map<sal_uInt16, SvxIconChoiceCtrlEntryPtrVec> IconChoiceMap; + +class IcnCursor_Impl +{ + SvxIconChoiceCtrl_Impl* pView; + std::unique_ptr<IconChoiceMap> xColumns; + std::unique_ptr<IconChoiceMap> xRows; + tools::Long nCols; + tools::Long nRows; + short nDeltaWidth; + short nDeltaHeight; + SvxIconChoiceCtrlEntry* pCurEntry; + void SetDeltas(); + void ImplCreate(); + void Create() { if( !xColumns ) ImplCreate(); } + + sal_uInt16 GetSortListPos( + SvxIconChoiceCtrlEntryPtrVec& rList, + tools::Long nValue, + bool bVertical); + SvxIconChoiceCtrlEntry* SearchCol( + sal_uInt16 nCol, + sal_uInt16 nTop, + sal_uInt16 nBottom, + bool bDown, + bool bSimple + ); + + SvxIconChoiceCtrlEntry* SearchRow( + sal_uInt16 nRow, + sal_uInt16 nLeft, + sal_uInt16 nRight, + bool bRight, + bool bSimple + ); + +public: + explicit IcnCursor_Impl( SvxIconChoiceCtrl_Impl* pOwner ); + ~IcnCursor_Impl(); + void Clear(); + + // for Cursortravelling etc. + SvxIconChoiceCtrlEntry* GoLeftRight( SvxIconChoiceCtrlEntry*, bool bRight ); + SvxIconChoiceCtrlEntry* GoUpDown( SvxIconChoiceCtrlEntry*, bool bDown ); + SvxIconChoiceCtrlEntry* GoPageUpDown( SvxIconChoiceCtrlEntry*, bool bDown ); +}; + +class IcnGridMap_Impl +{ + tools::Rectangle _aLastOccupiedGrid; + SvxIconChoiceCtrl_Impl* _pView; + std::unique_ptr<bool[]> _pGridMap; + sal_uInt16 _nGridCols, _nGridRows; + + void Expand(); + void Create_Impl(); + void Create() { if(!_pGridMap) Create_Impl(); } + + void GetMinMapSize( sal_uInt16& rDX, sal_uInt16& rDY ) const; + +public: + explicit IcnGridMap_Impl(SvxIconChoiceCtrl_Impl* pView); + ~IcnGridMap_Impl(); + + void Clear(); + + GridId GetGrid( const Point& rDocPos ); + GridId GetGrid( sal_uInt16 nGridX, sal_uInt16 nGridY ); + GridId GetUnoccupiedGrid(); + + void OccupyGrids( const SvxIconChoiceCtrlEntry* ); + void OccupyGrid( GridId nId ) + { + DBG_ASSERT(!_pGridMap || nId<o3tl::make_unsigned(_nGridCols*_nGridRows),"OccupyGrid: Bad GridId"); + if(_pGridMap && nId < o3tl::make_unsigned(_nGridCols *_nGridRows) ) + _pGridMap[ nId ] = true; + } + + tools::Rectangle GetGridRect( GridId ); + void GetGridCoord( GridId, sal_uInt16& rGridX, sal_uInt16& rGridY ); + static sal_uLong GetGridCount( + const Size& rSizePixel, + sal_uInt16 nGridWidth, + sal_uInt16 nGridHeight + ); + + void OutputSizeChanged(); +}; + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/imivctl1.cxx b/vcl/source/control/imivctl1.cxx new file mode 100644 index 000000000..953f90f2a --- /dev/null +++ b/vcl/source/control/imivctl1.cxx @@ -0,0 +1,2927 @@ +/* -*- 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 <limits.h> +#include <osl/diagnose.h> +#include <tools/debug.hxx> +#include <vcl/wall.hxx> +#include <vcl/help.hxx> +#include <vcl/decoview.hxx> +#include <vcl/event.hxx> +#include <vcl/svapp.hxx> +#include <tools/poly.hxx> +#include <vcl/lineinfo.hxx> +#include <vcl/i18nhelp.hxx> +#include <vcl/mnemonic.hxx> +#include <vcl/settings.hxx> +#include <vcl/commandevent.hxx> + +#include <vcl/toolkit/ivctrl.hxx> +#include "imivctl.hxx" + +#include <algorithm> +#include <memory> +#include <vcl/idle.hxx> + +constexpr auto DRAWTEXT_FLAGS_ICON = + DrawTextFlags::Center | DrawTextFlags::Top | DrawTextFlags::EndEllipsis | + DrawTextFlags::Clip | DrawTextFlags::MultiLine | DrawTextFlags::WordBreak | DrawTextFlags::Mnemonic; + +#define DRAWTEXT_FLAGS_SMALLICON (DrawTextFlags::Left|DrawTextFlags::EndEllipsis|DrawTextFlags::Clip) + +#define EVENTID_SHOW_CURSOR (reinterpret_cast<void*>(1)) +#define EVENTID_ADJUST_SCROLLBARS (reinterpret_cast<void*>(2)) + +SvxIconChoiceCtrl_Impl::SvxIconChoiceCtrl_Impl( + SvtIconChoiceCtrl* pCurView, + WinBits nWinStyle +) : + aVerSBar( VclPtr<ScrollBar>::Create(pCurView, WB_DRAG | WB_VSCROLL) ), + aHorSBar( VclPtr<ScrollBar>::Create(pCurView, WB_DRAG | WB_HSCROLL) ), + aScrBarBox( VclPtr<ScrollBarBox>::Create(pCurView) ), + aAutoArrangeIdle( "svtools::SvxIconChoiceCtrl_Impl aAutoArrangeIdle" ), + aDocRectChangedIdle( "svtools::SvxIconChoiceCtrl_Impl aDocRectChangedIdle" ), + aVisRectChangedIdle( "svtools::SvxIconChoiceCtrl_Impl aVisRectChangedIdle" ), + aCallSelectHdlIdle( "svtools::SvxIconChoiceCtrl_Impl aCallSelectHdlIdle" ), + aImageSize( 32 * pCurView->GetDPIScaleFactor(), 32 * pCurView->GetDPIScaleFactor()), + pView(pCurView), nMaxVirtWidth(DEFAULT_MAX_VIRT_WIDTH), nMaxVirtHeight(DEFAULT_MAX_VIRT_HEIGHT), + nFlags(IconChoiceFlags::NONE), nUserEventAdjustScrBars(nullptr), + pCurHighlightFrame(nullptr), bHighlightFramePressed(false), pHead(nullptr), pCursor(nullptr), + pHdlEntry(nullptr), + pAnchor(nullptr), eTextMode(SvxIconChoiceCtrlTextMode::Short), + eSelectionMode(SelectionMode::Multiple), ePositionMode(SvxIconChoiceCtrlPositionMode::Free), + bUpdateMode(true) +{ + SetStyle( nWinStyle ); + pImpCursor.reset( new IcnCursor_Impl( this ) ); + pGridMap.reset( new IcnGridMap_Impl( this ) ); + + aVerSBar->SetScrollHdl( LINK( this, SvxIconChoiceCtrl_Impl, ScrollUpDownHdl ) ); + aHorSBar->SetScrollHdl( LINK( this, SvxIconChoiceCtrl_Impl, ScrollLeftRightHdl ) ); + + nHorSBarHeight = aHorSBar->GetSizePixel().Height(); + nVerSBarWidth = aVerSBar->GetSizePixel().Width(); + + aAutoArrangeIdle.SetPriority( TaskPriority::HIGH_IDLE ); + aAutoArrangeIdle.SetInvokeHandler(LINK(this,SvxIconChoiceCtrl_Impl,AutoArrangeHdl)); + + aCallSelectHdlIdle.SetPriority( TaskPriority::LOWEST ); + aCallSelectHdlIdle.SetInvokeHandler( LINK(this,SvxIconChoiceCtrl_Impl,CallSelectHdlHdl)); + + aDocRectChangedIdle.SetPriority( TaskPriority::HIGH_IDLE ); + aDocRectChangedIdle.SetInvokeHandler(LINK(this,SvxIconChoiceCtrl_Impl,DocRectChangedHdl)); + + aVisRectChangedIdle.SetPriority( TaskPriority::HIGH_IDLE ); + aVisRectChangedIdle.SetInvokeHandler(LINK(this,SvxIconChoiceCtrl_Impl,VisRectChangedHdl)); + + Clear( true ); + Size gridSize(100,70); + if(pView->GetDPIScaleFactor() > 1) + { + gridSize.setHeight( gridSize.Height() * ( pView->GetDPIScaleFactor()) ); + } + SetGrid(gridSize); +} + +SvxIconChoiceCtrl_Impl::~SvxIconChoiceCtrl_Impl() +{ + Clear(false); + CancelUserEvents(); + pImpCursor.reset(); + pGridMap.reset(); + ClearSelectedRectList(); + m_pColumns.reset(); + aVerSBar.disposeAndClear(); + aHorSBar.disposeAndClear(); + aScrBarBox.disposeAndClear(); +} + +void SvxIconChoiceCtrl_Impl::Clear( bool bInCtor ) +{ + nSelectionCount = 0; + pCurHighlightFrame = nullptr; + CancelUserEvents(); + ShowCursor( false ); + bBoundRectsDirty = false; + nMaxBoundHeight = 0; + + pCursor = nullptr; + if( !bInCtor ) + { + pImpCursor->Clear(); + pGridMap->Clear(); + aVirtOutputSize.setWidth( 0 ); + aVirtOutputSize.setHeight( 0 ); + Size aSize( pView->GetOutputSizePixel() ); + nMaxVirtWidth = aSize.Width() - nVerSBarWidth; + if( nMaxVirtWidth <= 0 ) + nMaxVirtWidth = DEFAULT_MAX_VIRT_WIDTH; + nMaxVirtHeight = aSize.Height() - nHorSBarHeight; + if( nMaxVirtHeight <= 0 ) + nMaxVirtHeight = DEFAULT_MAX_VIRT_HEIGHT; + maZOrderList.clear(); + SetOrigin( Point() ); + if( bUpdateMode ) + pView->Invalidate(InvalidateFlags::NoChildren); + } + AdjustScrollBars(); + maEntries.clear(); + DocRectChanged(); + VisRectChanged(); +} + +void SvxIconChoiceCtrl_Impl::SetStyle( WinBits nWinStyle ) +{ + nWinBits = nWinStyle; + nCurTextDrawFlags = DRAWTEXT_FLAGS_ICON; + if( nWinBits & (WB_SMALLICON | WB_DETAILS) ) + nCurTextDrawFlags = DRAWTEXT_FLAGS_SMALLICON; + if( nWinBits & WB_NOSELECTION ) + eSelectionMode = SelectionMode::NONE; + if( !(nWinStyle & (WB_ALIGN_TOP | WB_ALIGN_LEFT))) + nWinBits |= WB_ALIGN_LEFT; + if( nWinStyle & WB_DETAILS ) + { + if (!m_pColumns) + SetColumn( 0, SvxIconChoiceCtrlColumnInfo() ); + } +} + +IMPL_LINK( SvxIconChoiceCtrl_Impl, ScrollUpDownHdl, ScrollBar*, pScrollBar, void ) +{ + // arrow up: delta=-1; arrow down: delta=+1 + Scroll( 0, pScrollBar->GetDelta() ); +} + +IMPL_LINK( SvxIconChoiceCtrl_Impl, ScrollLeftRightHdl, ScrollBar*, pScrollBar, void ) +{ + // arrow left: delta=-1; arrow right: delta=+1 + Scroll( pScrollBar->GetDelta(), 0 ); +} + +void SvxIconChoiceCtrl_Impl::FontModified() +{ + SetDefaultTextSize(); + ShowCursor( false ); + ShowCursor( true ); +} + +void SvxIconChoiceCtrl_Impl::InsertEntry( std::unique_ptr<SvxIconChoiceCtrlEntry> pEntry1, size_t nPos) +{ + auto pEntry = pEntry1.get(); + + if ( nPos < maEntries.size() ) { + maEntries.insert( maEntries.begin() + nPos, std::move(pEntry1) ); + } else { + maEntries.push_back( std::move(pEntry1) ); + } + + if( pHead ) + pEntry->SetBacklink( pHead->pblink ); + + if( (nFlags & IconChoiceFlags::EntryListPosValid) && nPos >= maEntries.size() - 1 ) + pEntry->nPos = maEntries.size() - 1; + else + nFlags &= ~IconChoiceFlags::EntryListPosValid; + + maZOrderList.push_back( pEntry ); + pImpCursor->Clear(); + + // If the UpdateMode is true, don't set all bounding rectangles to + // 'to be checked', but only the bounding rectangle of the new entry. + // Thus, don't call InvalidateBoundingRect! + pEntry->aRect.SetRight( LONG_MAX ); + if( bUpdateMode ) + { + FindBoundingRect( pEntry ); + tools::Rectangle aOutputArea( GetOutputRect() ); + pGridMap->OccupyGrids( pEntry ); + if( !aOutputArea.Overlaps( pEntry->aRect ) ) + return; // is invisible + pView->Invalidate( pEntry->aRect ); + } + else + InvalidateBoundingRect( pEntry->aRect ); +} + +void SvxIconChoiceCtrl_Impl::RemoveEntry(size_t nPos) +{ + pImpCursor->Clear(); + maEntries.erase(maEntries.begin() + nPos); + RecalcAllBoundingRectsSmart(); +} + +tools::Rectangle SvxIconChoiceCtrl_Impl::GetOutputRect() const +{ + Point aOrigin( pView->GetMapMode().GetOrigin() ); + aOrigin *= -1; + return tools::Rectangle( aOrigin, aOutputSize ); +} + +void SvxIconChoiceCtrl_Impl::SetListPositions() +{ + if( nFlags & IconChoiceFlags::EntryListPosValid ) + return; + + size_t nCount = maEntries.size(); + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + maEntries[ nCur ]->nPos = nCur; + } + nFlags |= IconChoiceFlags::EntryListPosValid; +} + +void SvxIconChoiceCtrl_Impl::SelectEntry( SvxIconChoiceCtrlEntry* pEntry, bool bSelect, + bool bAdd ) +{ + if( eSelectionMode == SelectionMode::NONE ) + return; + + if( !bAdd ) + { + if ( !( nFlags & IconChoiceFlags::ClearingSelection ) ) + { + nFlags |= IconChoiceFlags::ClearingSelection; + DeselectAllBut( pEntry ); + nFlags &= ~IconChoiceFlags::ClearingSelection; + } + } + if( pEntry->IsSelected() == bSelect ) + return; + + pHdlEntry = pEntry; + SvxIconViewFlags nEntryFlags = pEntry->GetFlags(); + if( bSelect ) + { + nEntryFlags |= SvxIconViewFlags::SELECTED; + pEntry->AssignFlags( nEntryFlags ); + nSelectionCount++; + CallSelectHandler(); + } + else + { + nEntryFlags &= ~SvxIconViewFlags::SELECTED; + pEntry->AssignFlags( nEntryFlags ); + nSelectionCount--; + CallSelectHandler(); + } + EntrySelected( pEntry, bSelect ); +} + +void SvxIconChoiceCtrl_Impl::EntrySelected(SvxIconChoiceCtrlEntry* pEntry, bool bSelect) +{ + // When using SingleSelection, make sure that the cursor is always placed + // over the (only) selected entry. (But only if a cursor exists.) + if (bSelect && pCursor && + eSelectionMode == SelectionMode::Single && + pEntry != pCursor) + { + SetCursor(pEntry); + } + + // Not when dragging though, else the loop in SelectRect doesn't work + // correctly! + if (!(nFlags & IconChoiceFlags::SelectingRect)) + ToTop(pEntry); + if (bUpdateMode) + { + if (pEntry == pCursor) + ShowCursor(false); + pView->Invalidate(CalcFocusRect(pEntry)); + if (pEntry == pCursor) + ShowCursor(true); + } + + // #i101012# emit vcl event LISTBOX_SELECT only in case that the given entry is selected. + if (bSelect) + { + CallEventListeners(VclEventId::ListboxSelect, pEntry); + } +} + +void SvxIconChoiceCtrl_Impl::ResetVirtSize() +{ + aVirtOutputSize.setWidth( 0 ); + aVirtOutputSize.setHeight( 0 ); + const size_t nCount = maEntries.size(); + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pCur = maEntries[ nCur ].get(); + pCur->ClearFlags( SvxIconViewFlags::POS_MOVED ); + if( pCur->IsPosLocked() ) + { + // adapt (among others) VirtSize + if( !IsBoundingRectValid( pCur->aRect ) ) + FindBoundingRect( pCur ); + else + AdjustVirtSize( pCur->aRect ); + } + else + InvalidateBoundingRect( pCur->aRect ); + } + + if( !(nWinBits & (WB_NOVSCROLL | WB_NOHSCROLL)) ) + { + Size aRealOutputSize( pView->GetOutputSizePixel() ); + if( aVirtOutputSize.Width() < aRealOutputSize.Width() || + aVirtOutputSize.Height() < aRealOutputSize.Height() ) + { + sal_uLong nGridCount = IcnGridMap_Impl::GetGridCount( + aRealOutputSize, static_cast<sal_uInt16>(nGridDX), static_cast<sal_uInt16>(nGridDY) ); + if( nGridCount < nCount ) + { + if( nWinBits & WB_ALIGN_TOP ) + nMaxVirtWidth = aRealOutputSize.Width() - nVerSBarWidth; + else // WB_ALIGN_LEFT + nMaxVirtHeight = aRealOutputSize.Height() - nHorSBarHeight; + } + } + } + + pImpCursor->Clear(); + pGridMap->Clear(); + VisRectChanged(); +} + +void SvxIconChoiceCtrl_Impl::AdjustVirtSize( const tools::Rectangle& rRect ) +{ + tools::Long nHeightOffs = 0; + tools::Long nWidthOffs = 0; + + if( aVirtOutputSize.Width() < (rRect.Right()+LROFFS_WINBORDER) ) + nWidthOffs = (rRect.Right()+LROFFS_WINBORDER) - aVirtOutputSize.Width(); + + if( aVirtOutputSize.Height() < (rRect.Bottom()+TBOFFS_WINBORDER) ) + nHeightOffs = (rRect.Bottom()+TBOFFS_WINBORDER) - aVirtOutputSize.Height(); + + if( !(nWidthOffs || nHeightOffs) ) + return; + + Range aRange; + aVirtOutputSize.AdjustWidth(nWidthOffs ); + aRange.Max() = aVirtOutputSize.Width(); + aHorSBar->SetRange( aRange ); + + aVirtOutputSize.AdjustHeight(nHeightOffs ); + aRange.Max() = aVirtOutputSize.Height(); + aVerSBar->SetRange( aRange ); + + pImpCursor->Clear(); + pGridMap->OutputSizeChanged(); + AdjustScrollBars(); + DocRectChanged(); +} + +void SvxIconChoiceCtrl_Impl::InitPredecessors() +{ + DBG_ASSERT(!pHead,"SvxIconChoiceCtrl_Impl::InitPredecessors() >> Already initialized"); + size_t nCount = maEntries.size(); + if( nCount ) + { + SvxIconChoiceCtrlEntry* pPrev = maEntries[ 0 ].get(); + for( size_t nCur = 1; nCur <= nCount; nCur++ ) + { + pPrev->ClearFlags( SvxIconViewFlags::POS_LOCKED | SvxIconViewFlags::POS_MOVED ); + + SvxIconChoiceCtrlEntry* pNext; + if( nCur == nCount ) + pNext = maEntries[ 0 ].get(); + else + pNext = maEntries[ nCur ].get(); + pPrev->pflink = pNext; + pNext->pblink = pPrev; + pPrev = pNext; + } + pHead = maEntries[ 0 ].get(); + } + else + pHead = nullptr; +} + +void SvxIconChoiceCtrl_Impl::ClearPredecessors() +{ + if( pHead ) + { + size_t nCount = maEntries.size(); + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pCur = maEntries[ nCur ].get(); + pCur->pflink = nullptr; + pCur->pblink = nullptr; + } + pHead = nullptr; + } +} + +void SvxIconChoiceCtrl_Impl::Arrange( bool bKeepPredecessors, tools::Long nSetMaxVirtWidth, tools::Long nSetMaxVirtHeight ) +{ + if ( nSetMaxVirtWidth != 0 ) + nMaxVirtWidth = nSetMaxVirtWidth; + else + nMaxVirtWidth = aOutputSize.Width(); + + if ( nSetMaxVirtHeight != 0 ) + nMaxVirtHeight = nSetMaxVirtHeight; + else + nMaxVirtHeight = aOutputSize.Height(); + + ImpArrange( bKeepPredecessors ); +} + +void SvxIconChoiceCtrl_Impl::ImpArrange( bool bKeepPredecessors ) +{ + static const Point aEmptyPoint; + + bool bOldUpdate = bUpdateMode; + tools::Rectangle aCurOutputArea( GetOutputRect() ); + if( (nWinBits & WB_SMART_ARRANGE) && aCurOutputArea.TopLeft() != aEmptyPoint ) + bUpdateMode = false; + aAutoArrangeIdle.Stop(); + nFlags |= IconChoiceFlags::Arranging; + ShowCursor( false ); + ResetVirtSize(); + if( !bKeepPredecessors ) + ClearPredecessors(); + bBoundRectsDirty = false; + SetOrigin( Point() ); + VisRectChanged(); + RecalcAllBoundingRectsSmart(); + // TODO: the invalidation in the detail view should be more intelligent + //if( !(nWinBits & WB_DETAILS )) + pView->Invalidate( InvalidateFlags::NoChildren ); + nFlags &= ~IconChoiceFlags::Arranging; + if( (nWinBits & WB_SMART_ARRANGE) && aCurOutputArea.TopLeft() != aEmptyPoint ) + { + MakeVisible( aCurOutputArea ); + SetUpdateMode( bOldUpdate ); + } + ShowCursor( true ); +} + +void SvxIconChoiceCtrl_Impl::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ +#if defined(OV_DRAWGRID) + Color aOldColor (rRenderContext.GetLineColor()); + Color aCOL_BLACK); + rRenderContext.SetLineColor( aColor ); + Point aOffs(rRenderContext.GetMapMode().GetOrigin()); + Size aXSize(GetOutputSizePixel()); + { + Point aStart(LROFFS_WINBORDER, 0); + Point aEnd(LROFFS_WINBORDER, aXSize.Height()); + aStart -= aOffs; + aEnd -= aOffs; + rRenderContext.DrawLine(aStart, aEnd); + } + { + Point aStart(0, TBOFFS_WINBORDER); + Point aEnd(aXSize.Width(), TBOFFS_WINBORDER); + aStart -= aOffs; + aEnd -= aOffs; + rRenderContext.DrawLine(aStart, aEnd); + } + + for (tools::Long nDX = nGridDX; nDX <= aXSize.Width(); nDX += nGridDX) + { + Point aStart( nDX+LROFFS_WINBORDER, 0 ); + Point aEnd( nDX+LROFFS_WINBORDER, aXSize.Height()); + aStart -= aOffs; + aEnd -= aOffs; + rRenderContext.DrawLine(aStart, aEnd); + } + for (tools::Long nDY = nGridDY; nDY <= aXSize.Height(); nDY += nGridDY) + { + Point aStart(0, nDY + TBOFFS_WINBORDER); + Point aEnd(aXSize.Width(), nDY + TBOFFS_WINBORDER); + aStart -= aOffs; + aEnd -= aOffs; + rRenderContext.DrawLine(aStart, aEnd); + } + rRenderContext.SetLineColor(aOldColor); +#endif + + if (!maEntries.size()) + return; + if (!pCursor) + { + // set cursor to item with focus-flag + bool bfound = false; + for (sal_Int32 i = 0; i < pView->GetEntryCount() && !bfound; i++) + { + SvxIconChoiceCtrlEntry* pEntry = pView->GetEntry(i); + if (pEntry->IsFocused()) + { + pCursor = pEntry; + bfound = true; + } + } + + if (!bfound) + pCursor = maEntries[ 0 ].get(); + } + + size_t nCount = maZOrderList.size(); + if (!nCount) + return; + + rRenderContext.Push(vcl::PushFlags::CLIPREGION); + rRenderContext.SetClipRegion(vcl::Region(rRect)); + + std::vector< SvxIconChoiceCtrlEntry* > aNewZOrderList; + std::vector< SvxIconChoiceCtrlEntry* > aPaintedEntries; + + size_t nPos = 0; + while(nCount) + { + SvxIconChoiceCtrlEntry* pEntry = maZOrderList[nPos]; + const tools::Rectangle& rBoundRect = GetEntryBoundRect(pEntry); + if (rRect.Overlaps(rBoundRect)) + { + PaintEntry(pEntry, rBoundRect.TopLeft(), rRenderContext); + // set entries to Top if they are being repainted + aPaintedEntries.push_back(pEntry); + } + else + aNewZOrderList.push_back(pEntry); + + nCount--; + nPos++; + } + maZOrderList = std::move( aNewZOrderList ); + maZOrderList.insert(maZOrderList.end(), aPaintedEntries.begin(), aPaintedEntries.end()); + + rRenderContext.Pop(); +} + +void SvxIconChoiceCtrl_Impl::RepaintSelectedEntries() +{ + const size_t nCount = maZOrderList.size(); + if (!nCount) + return; + + tools::Rectangle aOutRect(GetOutputRect()); + for (size_t nCur = 0; nCur < nCount; nCur++) + { + SvxIconChoiceCtrlEntry* pEntry = maZOrderList[nCur]; + if (pEntry->GetFlags() & SvxIconViewFlags::SELECTED) + { + const tools::Rectangle& rBoundRect = GetEntryBoundRect(pEntry); + if (aOutRect.Overlaps(rBoundRect)) + pView->Invalidate(rBoundRect); + } + } +} + +void SvxIconChoiceCtrl_Impl::InitScrollBarBox() +{ + aScrBarBox->SetSizePixel( Size(nVerSBarWidth-1, nHorSBarHeight-1) ); + Size aSize( pView->GetOutputSizePixel() ); + aScrBarBox->SetPosPixel( Point(aSize.Width()-nVerSBarWidth+1, aSize.Height()-nHorSBarHeight+1)); +} + +bool SvxIconChoiceCtrl_Impl::MouseButtonDown( const MouseEvent& rMEvt) +{ + bool bHandled = true; + bHighlightFramePressed = false; + bool bGotFocus = (!pView->HasFocus() && !(nWinBits & WB_NOPOINTERFOCUS)); + if( !(nWinBits & WB_NOPOINTERFOCUS) ) + pView->GrabFocus(); + + Point aDocPos( rMEvt.GetPosPixel() ); + if(aDocPos.X()>=aOutputSize.Width() || aDocPos.Y()>=aOutputSize.Height()) + return false; + ToDocPos( aDocPos ); + SvxIconChoiceCtrlEntry* pEntry = GetEntry( aDocPos, true ); + if( pEntry ) + MakeEntryVisible( pEntry, false ); + + if( rMEvt.IsShift() && eSelectionMode != SelectionMode::Single ) + { + if( pEntry ) + SetCursor_Impl( pCursor, pEntry, rMEvt.IsMod1(), rMEvt.IsShift() ); + return true; + } + + if( pAnchor && (rMEvt.IsShift() || rMEvt.IsMod1())) // keyboard selection? + { + DBG_ASSERT(eSelectionMode != SelectionMode::Single,"Invalid selection mode"); + if( rMEvt.IsMod1() ) + nFlags |= IconChoiceFlags::AddMode; + + if( rMEvt.IsShift() ) + { + tools::Rectangle aRect( GetEntryBoundRect( pAnchor )); + if( pEntry ) + aRect.Union( GetEntryBoundRect( pEntry ) ); + else + { + tools::Rectangle aTempRect( aDocPos, Size(1,1)); + aRect.Union( aTempRect ); + } + aCurSelectionRect = aRect; + SelectRect( aRect, bool(nFlags & IconChoiceFlags::AddMode), &aSelectedRectList ); + } + else if( rMEvt.IsMod1() ) + { + AddSelectedRect( aCurSelectionRect ); + pAnchor = nullptr; + aCurSelectionRect.SetPos( aDocPos ); + } + + if( !pEntry && !(nWinBits & WB_NODRAGSELECTION)) + pView->StartTracking( StartTrackingFlags::ScrollRepeat ); + return true; + } + else + { + if( !pEntry ) + { + if( eSelectionMode == SelectionMode::Multiple ) + { + if( !rMEvt.IsMod1() ) // Ctrl + { + if( !bGotFocus ) + { + SetNoSelection(); + ClearSelectedRectList(); + } + } + else + nFlags |= IconChoiceFlags::AddMode; + aCurSelectionRect.SetPos( aDocPos ); + pView->StartTracking( StartTrackingFlags::ScrollRepeat ); + } + else + bHandled = false; + return bHandled; + } + } + bool bSelected = pEntry->IsSelected(); + + if( rMEvt.GetClicks() == 2 ) + { + DeselectAllBut( pEntry ); + SelectEntry( pEntry, true, false ); + pHdlEntry = pEntry; + pView->ClickIcon(); + } + else + { + // Inplace-Editing ? + if( rMEvt.IsMod2() ) // Alt? + { + } + else if( eSelectionMode == SelectionMode::Single ) + { + DeselectAllBut( pEntry ); + SetCursor( pEntry ); + } + else if( eSelectionMode == SelectionMode::NONE ) + { + if( rMEvt.IsLeft() && (nWinBits & WB_HIGHLIGHTFRAME) ) + { + pCurHighlightFrame = nullptr; // force repaint of frame + bHighlightFramePressed = true; + SetEntryHighlightFrame( pEntry, true ); + } + } + else + { + if( !rMEvt.GetModifier() && rMEvt.IsLeft() ) + { + if( !bSelected ) + { + DeselectAllBut( pEntry ); + SetCursor( pEntry ); + SelectEntry( pEntry, true, false ); + } + else + { + // deselect only in the Up, if the Move happened via D&D! + nFlags |= IconChoiceFlags::DownDeselect; + } + } + else if( rMEvt.IsMod1() ) + nFlags |= IconChoiceFlags::DownCtrl; + } + } + return bHandled; +} + +bool SvxIconChoiceCtrl_Impl::MouseButtonUp( const MouseEvent& rMEvt ) +{ + bool bHandled = false; + if( rMEvt.IsRight() && (nFlags & (IconChoiceFlags::DownCtrl | IconChoiceFlags::DownDeselect) )) + { + nFlags &= ~IconChoiceFlags(IconChoiceFlags::DownCtrl | IconChoiceFlags::DownDeselect); + bHandled = true; + } + + Point aDocPos( rMEvt.GetPosPixel() ); + ToDocPos( aDocPos ); + SvxIconChoiceCtrlEntry* pDocEntry = GetEntry( aDocPos ); + if( pDocEntry ) + { + if( nFlags & IconChoiceFlags::DownCtrl ) + { + // Ctrl & MultiSelection + ToggleSelection( pDocEntry ); + SetCursor( pDocEntry ); + bHandled = true; + } + else if( nFlags & IconChoiceFlags::DownDeselect ) + { + DeselectAllBut( pDocEntry ); + SetCursor( pDocEntry ); + SelectEntry( pDocEntry, true, false ); + bHandled = true; + } + } + + nFlags &= ~IconChoiceFlags(IconChoiceFlags::DownCtrl | IconChoiceFlags::DownDeselect); + + if((nWinBits & WB_HIGHLIGHTFRAME) && bHighlightFramePressed && pCurHighlightFrame) + { + bHandled = true; + SvxIconChoiceCtrlEntry* pEntry = pCurHighlightFrame; + pCurHighlightFrame = nullptr; // force repaint of frame + bHighlightFramePressed = false; + SetEntryHighlightFrame( pEntry, true ); + + pHdlEntry = pCurHighlightFrame; + pView->ClickIcon(); + + // set focus on Icon + SvxIconChoiceCtrlEntry* pOldCursor = pCursor; + SetCursor_Impl( pOldCursor, pHdlEntry, false, false ); + + pHdlEntry = nullptr; + } + return bHandled; +} + +bool SvxIconChoiceCtrl_Impl::MouseMove( const MouseEvent& rMEvt ) +{ + const Point aDocPos( pView->PixelToLogic(rMEvt.GetPosPixel()) ); + + if( pView->IsTracking() ) + return false; + else if( nWinBits & WB_HIGHLIGHTFRAME ) + { + SvxIconChoiceCtrlEntry* pEntry = GetEntry( aDocPos, true ); + SetEntryHighlightFrame( pEntry, false ); + } + else + return false; + return true; +} + +void SvxIconChoiceCtrl_Impl::SetCursor_Impl( SvxIconChoiceCtrlEntry* pOldCursor, + SvxIconChoiceCtrlEntry* pNewCursor, bool bMod1, bool bShift ) +{ + if( !pNewCursor ) + return; + + SvxIconChoiceCtrlEntry* pFilterEntry = nullptr; + bool bDeselectAll = false; + if( eSelectionMode != SelectionMode::Single ) + { + if( !bMod1 && !bShift ) + bDeselectAll = true; + else if( bShift && !bMod1 && !pAnchor ) + { + bDeselectAll = true; + pFilterEntry = pOldCursor; + } + } + if( bDeselectAll ) + DeselectAllBut( pFilterEntry ); + ShowCursor( false ); + MakeEntryVisible( pNewCursor ); + SetCursor( pNewCursor ); + if( bMod1 && !bShift ) + { + if( pAnchor ) + { + AddSelectedRect( pAnchor, pOldCursor ); + pAnchor = nullptr; + } + } + else if( bShift ) + { + if( !pAnchor ) + pAnchor = pOldCursor; + if ( nWinBits & WB_ALIGN_LEFT ) + SelectRange( pAnchor, pNewCursor, bool(nFlags & IconChoiceFlags::AddMode) ); + else + SelectRect(pAnchor,pNewCursor, bool(nFlags & IconChoiceFlags::AddMode), &aSelectedRectList); + } + else + { + SelectEntry( pCursor, true, false ); + aCurSelectionRect = GetEntryBoundRect( pCursor ); + CallEventListeners( VclEventId::ListboxSelect, pCursor ); + } +} + +bool SvxIconChoiceCtrl_Impl::KeyInput( const KeyEvent& rKEvt ) +{ + bool bMod2 = rKEvt.GetKeyCode().IsMod2(); + sal_Unicode cChar = rKEvt.GetCharCode(); + sal_uLong nPos = sal_uLong(-1); + if ( bMod2 && cChar && IsMnemonicChar( cChar, nPos ) ) + { + // shortcut is clicked + SvxIconChoiceCtrlEntry* pNewCursor = GetEntry( nPos ); + SvxIconChoiceCtrlEntry* pOldCursor = pCursor; + if ( pNewCursor != pOldCursor ) + SetCursor_Impl( pOldCursor, pNewCursor, false, false ); + return true; + } + + if ( bMod2 ) + // no actions with <ALT> + return false; + + bool bKeyUsed = true; + bool bMod1 = rKEvt.GetKeyCode().IsMod1(); + bool bShift = rKEvt.GetKeyCode().IsShift(); + + if( eSelectionMode == SelectionMode::Single || eSelectionMode == SelectionMode::NONE) + { + bShift = false; + bMod1 = false; + } + + if( bMod1 ) + nFlags |= IconChoiceFlags::AddMode; + + SvxIconChoiceCtrlEntry* pNewCursor; + SvxIconChoiceCtrlEntry* pOldCursor = pCursor; + + sal_uInt16 nCode = rKEvt.GetKeyCode().GetCode(); + switch( nCode ) + { + case KEY_UP: + case KEY_PAGEUP: + if( pCursor ) + { + MakeEntryVisible( pCursor ); + if( nCode == KEY_UP ) + pNewCursor = pImpCursor->GoUpDown(pCursor,false); + else + pNewCursor = pImpCursor->GoPageUpDown(pCursor,false); + SetCursor_Impl( pOldCursor, pNewCursor, bMod1, bShift ); + if( !pNewCursor ) + { + tools::Rectangle aRect( GetEntryBoundRect( pCursor ) ); + if( aRect.Top()) + { + aRect.AdjustBottom( -(aRect.Top()) ); + aRect.SetTop( 0 ); + MakeVisible( aRect ); + } + } + } + break; + + case KEY_DOWN: + case KEY_PAGEDOWN: + if( pCursor ) + { + if( nCode == KEY_DOWN ) + pNewCursor=pImpCursor->GoUpDown( pCursor,true ); + else + pNewCursor=pImpCursor->GoPageUpDown( pCursor,true ); + SetCursor_Impl( pOldCursor, pNewCursor, bMod1, bShift ); + } + break; + + case KEY_RIGHT: + if( pCursor ) + { + pNewCursor=pImpCursor->GoLeftRight(pCursor,true ); + SetCursor_Impl( pOldCursor, pNewCursor, bMod1, bShift ); + } + break; + + case KEY_LEFT: + if( pCursor ) + { + MakeEntryVisible( pCursor ); + pNewCursor = pImpCursor->GoLeftRight(pCursor,false ); + SetCursor_Impl( pOldCursor, pNewCursor, bMod1, bShift ); + if( !pNewCursor ) + { + tools::Rectangle aRect( GetEntryBoundRect(pCursor)); + if( aRect.Left() ) + { + aRect.AdjustRight( -(aRect.Left()) ); + aRect.SetLeft( 0 ); + MakeVisible( aRect ); + } + } + } + break; + + case KEY_F2: + if( bMod1 || bShift ) + bKeyUsed = false; + break; + + case KEY_F8: + if( rKEvt.GetKeyCode().IsShift() ) + { + if( nFlags & IconChoiceFlags::AddMode ) + nFlags &= ~IconChoiceFlags::AddMode; + else + nFlags |= IconChoiceFlags::AddMode; + } + else + bKeyUsed = false; + break; + + case KEY_SPACE: + if( pCursor && eSelectionMode != SelectionMode::Single ) + { + if( !bMod1 ) + { + //SelectAll( false ); + SetNoSelection(); + ClearSelectedRectList(); + + // click Icon with spacebar + SetEntryHighlightFrame( GetCurEntry(), true ); + pView->ClickIcon(); + pHdlEntry = pCurHighlightFrame; + pCurHighlightFrame=nullptr; + } + else + ToggleSelection( pCursor ); + } + break; + +#ifdef DBG_UTIL + case KEY_F10: + if( rKEvt.GetKeyCode().IsShift() ) + { + if( pCursor ) + pView->SetEntryTextMode( SvxIconChoiceCtrlTextMode::Full, pCursor ); + } + if( rKEvt.GetKeyCode().IsMod1() ) + { + if( pCursor ) + pView->SetEntryTextMode( SvxIconChoiceCtrlTextMode::Short, pCursor ); + } + break; +#endif + + case KEY_ADD: + case KEY_DIVIDE : + case KEY_A: + if( bMod1 && (eSelectionMode != SelectionMode::Single)) + SelectAll(); + else + bKeyUsed = false; + break; + + case KEY_SUBTRACT: + case KEY_COMMA : + if( bMod1 ) + SetNoSelection(); + else + bKeyUsed = false; + break; + + case KEY_RETURN: + if( !bMod1 ) + bKeyUsed = false; + break; + + case KEY_END: + if( pCursor ) + { + pNewCursor = maEntries.back().get(); + SetCursor_Impl( pOldCursor, pNewCursor, bMod1, bShift ); + } + break; + + case KEY_HOME: + if( pCursor ) + { + pNewCursor = maEntries[ 0 ].get(); + SetCursor_Impl( pOldCursor, pNewCursor, bMod1, bShift ); + } + break; + + default: + bKeyUsed = false; + + } + return bKeyUsed; +} + +// recalculate TopLeft of scrollbars (but not their sizes!) +void SvxIconChoiceCtrl_Impl::PositionScrollBars( tools::Long nRealWidth, tools::Long nRealHeight ) +{ + // horizontal scrollbar + Point aPos( 0, nRealHeight ); + aPos.AdjustY( -nHorSBarHeight ); + + if( aHorSBar->GetPosPixel() != aPos ) + aHorSBar->SetPosPixel( aPos ); + + // vertical scrollbar + aPos.setX( nRealWidth ); aPos.setY( 0 ); + aPos.AdjustX( -nVerSBarWidth ); + aPos.AdjustX( 1 ); + aPos.AdjustY( -1 ); + + if( aVerSBar->GetPosPixel() != aPos ) + aVerSBar->SetPosPixel( aPos ); +} + +void SvxIconChoiceCtrl_Impl::AdjustScrollBars() +{ + tools::Long nVirtHeight = aVirtOutputSize.Height(); + tools::Long nVirtWidth = aVirtOutputSize.Width(); + + Size aOSize( pView->Control::GetOutputSizePixel() ); + tools::Long nRealHeight = aOSize.Height(); + tools::Long nRealWidth = aOSize.Width(); + + PositionScrollBars( nRealWidth, nRealHeight ); + + const MapMode& rMapMode = pView->GetMapMode(); + Point aOrigin( rMapMode.GetOrigin() ); + + tools::Long nVisibleWidth; + if( nRealWidth > nVirtWidth ) + nVisibleWidth = nVirtWidth + aOrigin.X(); + else + nVisibleWidth = nRealWidth; + + tools::Long nVisibleHeight; + if( nRealHeight > nVirtHeight ) + nVisibleHeight = nVirtHeight + aOrigin.Y(); + else + nVisibleHeight = nRealHeight; + + bool bVerSBar = ( nWinBits & WB_VSCROLL ) != 0; + bool bHorSBar = ( nWinBits & WB_HSCROLL ) != 0; + bool bNoVerSBar = ( nWinBits & WB_NOVSCROLL ) != 0; + bool bNoHorSBar = ( nWinBits & WB_NOHSCROLL ) != 0; + + sal_uInt16 nResult = 0; + if( nVirtHeight ) + { + // activate vertical scrollbar? + if( !bNoVerSBar && (bVerSBar || ( nVirtHeight > nVisibleHeight)) ) + { + nResult = 0x0001; + nRealWidth -= nVerSBarWidth; + + if( nRealWidth > nVirtWidth ) + nVisibleWidth = nVirtWidth + aOrigin.X(); + else + nVisibleWidth = nRealWidth; + } + // activate horizontal scrollbar? + if( !bNoHorSBar && (bHorSBar || (nVirtWidth > nVisibleWidth)) ) + { + nResult |= 0x0002; + nRealHeight -= nHorSBarHeight; + + if( nRealHeight > nVirtHeight ) + nVisibleHeight = nVirtHeight + aOrigin.Y(); + else + nVisibleHeight = nRealHeight; + + // do we need a vertical scrollbar after all? + if( !(nResult & 0x0001) && // only if not already there + ( !bNoVerSBar && ((nVirtHeight > nVisibleHeight) || bVerSBar)) ) + { + nResult = 3; // both turned on + nRealWidth -= nVerSBarWidth; + + if( nRealWidth > nVirtWidth ) + nVisibleWidth = nVirtWidth + aOrigin.X(); + else + nVisibleWidth = nRealWidth; + } + } + } + + // size vertical scrollbar + tools::Long nThumb = aVerSBar->GetThumbPos(); + Size aSize( nVerSBarWidth, nRealHeight ); + aSize.AdjustHeight(2 ); + if( aSize != aVerSBar->GetSizePixel() ) + aVerSBar->SetSizePixel( aSize ); + aVerSBar->SetVisibleSize( nVisibleHeight ); + aVerSBar->SetPageSize( GetScrollBarPageSize( nVisibleHeight )); + + if( nResult & 0x0001 ) + { + aVerSBar->SetThumbPos( nThumb ); + aVerSBar->Show(); + } + else + { + aVerSBar->SetThumbPos( 0 ); + aVerSBar->Hide(); + } + + // size horizontal scrollbar + nThumb = aHorSBar->GetThumbPos(); + aSize.setWidth( nRealWidth ); + aSize.setHeight( nHorSBarHeight ); + aSize.AdjustWidth( 1 ); + if( nResult & 0x0001 ) // vertical scrollbar? + { + aSize.AdjustWidth( 1 ); + nRealWidth++; + } + if( aSize != aHorSBar->GetSizePixel() ) + aHorSBar->SetSizePixel( aSize ); + aHorSBar->SetVisibleSize( nVisibleWidth ); + aHorSBar->SetPageSize( GetScrollBarPageSize(nVisibleWidth )); + if( nResult & 0x0002 ) + { + aHorSBar->SetThumbPos( nThumb ); + aHorSBar->Show(); + } + else + { + aHorSBar->SetThumbPos( 0 ); + aHorSBar->Hide(); + } + + aOutputSize.setWidth( nRealWidth ); + if( nResult & 0x0002 ) // horizontal scrollbar ? + nRealHeight++; // because lower border is clipped + aOutputSize.setHeight( nRealHeight ); + + if( (nResult & (0x0001|0x0002)) == (0x0001|0x0002) ) + aScrBarBox->Show(); + else + aScrBarBox->Hide(); +} + +void SvxIconChoiceCtrl_Impl::Resize() +{ + InitScrollBarBox(); + aOutputSize = pView->GetOutputSizePixel(); + pImpCursor->Clear(); + pGridMap->OutputSizeChanged(); + + const Size& rSize = pView->Control::GetOutputSizePixel(); + PositionScrollBars( rSize.Width(), rSize.Height() ); + // The scrollbars are shown/hidden asynchronously, so derived classes can + // do an Arrange during Resize, without the scrollbars suddenly turning + // on and off again. + // If an event is already underway, we don't need to send a new one, at least + // as long as there is only one event type. + if ( ! nUserEventAdjustScrBars ) + nUserEventAdjustScrBars = + Application::PostUserEvent( LINK( this, SvxIconChoiceCtrl_Impl, UserEventHdl), + EVENTID_ADJUST_SCROLLBARS); + + VisRectChanged(); +} + +bool SvxIconChoiceCtrl_Impl::CheckHorScrollBar() +{ + if( maZOrderList.empty() || !aHorSBar->IsVisible() ) + return false; + const MapMode& rMapMode = pView->GetMapMode(); + Point aOrigin( rMapMode.GetOrigin() ); + if(!( nWinBits & WB_HSCROLL) && !aOrigin.X() ) + { + tools::Long nWidth = aOutputSize.Width(); + const size_t nCount = maZOrderList.size(); + tools::Long nMostRight = 0; + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = maZOrderList[ nCur ]; + tools::Long nRight = GetEntryBoundRect(pEntry).Right(); + if( nRight > nWidth ) + return false; + if( nRight > nMostRight ) + nMostRight = nRight; + } + aHorSBar->Hide(); + aOutputSize.AdjustHeight(nHorSBarHeight ); + aVirtOutputSize.setWidth( nMostRight ); + aHorSBar->SetThumbPos( 0 ); + Range aRange; + aRange.Max() = nMostRight - 1; + aHorSBar->SetRange( aRange ); + if( aVerSBar->IsVisible() ) + { + Size aSize( aVerSBar->GetSizePixel()); + aSize.AdjustHeight(nHorSBarHeight ); + aVerSBar->SetSizePixel( aSize ); + } + return true; + } + return false; +} + +bool SvxIconChoiceCtrl_Impl::CheckVerScrollBar() +{ + if( maZOrderList.empty() || !aVerSBar->IsVisible() ) + return false; + const MapMode& rMapMode = pView->GetMapMode(); + Point aOrigin( rMapMode.GetOrigin() ); + if(!( nWinBits & WB_VSCROLL) && !aOrigin.Y() ) + { + tools::Long nDeepest = 0; + tools::Long nHeight = aOutputSize.Height(); + const size_t nCount = maZOrderList.size(); + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = maZOrderList[ nCur ]; + tools::Long nBottom = GetEntryBoundRect(pEntry).Bottom(); + if( nBottom > nHeight ) + return false; + if( nBottom > nDeepest ) + nDeepest = nBottom; + } + aVerSBar->Hide(); + aOutputSize.AdjustWidth(nVerSBarWidth ); + aVirtOutputSize.setHeight( nDeepest ); + aVerSBar->SetThumbPos( 0 ); + Range aRange; + aRange.Max() = nDeepest - 1; + aVerSBar->SetRange( aRange ); + if( aHorSBar->IsVisible() ) + { + Size aSize( aHorSBar->GetSizePixel()); + aSize.AdjustWidth(nVerSBarWidth ); + aHorSBar->SetSizePixel( aSize ); + } + return true; + } + return false; +} + + +// hides scrollbars if they're unnecessary +void SvxIconChoiceCtrl_Impl::CheckScrollBars() +{ + CheckVerScrollBar(); + if( CheckHorScrollBar() ) + CheckVerScrollBar(); + if( aVerSBar->IsVisible() && aHorSBar->IsVisible() ) + aScrBarBox->Show(); + else + aScrBarBox->Hide(); +} + + +void SvxIconChoiceCtrl_Impl::GetFocus() +{ + RepaintSelectedEntries(); + if( pCursor ) + { + pCursor->SetFlags( SvxIconViewFlags::FOCUSED ); + ShowCursor( true ); + } +} + +void SvxIconChoiceCtrl_Impl::LoseFocus() +{ + if( pCursor ) + pCursor->ClearFlags( SvxIconViewFlags::FOCUSED ); + ShowCursor( false ); + +// HideFocus (); +// pView->Invalidate ( aFocus.aRect ); + + RepaintSelectedEntries(); +} + +void SvxIconChoiceCtrl_Impl::SetUpdateMode( bool bUpdate ) +{ + if( bUpdate != bUpdateMode ) + { + bUpdateMode = bUpdate; + if( bUpdate ) + { + AdjustScrollBars(); + pImpCursor->Clear(); + pGridMap->Clear(); + pView->Invalidate(InvalidateFlags::NoChildren); + } + } +} + +// priorities of the emphasis: bSelected +void SvxIconChoiceCtrl_Impl::PaintEmphasis(const tools::Rectangle& rTextRect, bool bSelected, + vcl::RenderContext& rRenderContext) +{ + Color aOldFillColor(rRenderContext.GetFillColor()); + + bool bSolidTextRect = false; + + if (!bSelected) + { + const Color& rFillColor = rRenderContext.GetFont().GetFillColor(); + rRenderContext.SetFillColor(rFillColor); + if (rFillColor != COL_TRANSPARENT) + bSolidTextRect = true; + } + + // draw text rectangle + if (bSolidTextRect) + { + rRenderContext.DrawRect(rTextRect); + } + + rRenderContext.SetFillColor(aOldFillColor); +} + + +void SvxIconChoiceCtrl_Impl::PaintItem(const tools::Rectangle& rRect, + IcnViewFieldType eItem, SvxIconChoiceCtrlEntry* pEntry, sal_uInt16 nPaintFlags, + vcl::RenderContext& rRenderContext ) +{ + if (eItem == IcnViewFieldType::Text) + { + OUString aText = SvtIconChoiceCtrl::GetEntryText(pEntry); + + rRenderContext.DrawText(rRect, aText, nCurTextDrawFlags); + + if (pEntry->IsFocused()) + { + tools::Rectangle aRect (CalcFocusRect(pEntry)); + ShowFocus(aRect); + DrawFocusRect(rRenderContext); + } + } + else + { + Point aPos(rRect.TopLeft()); + if (nPaintFlags & PAINTFLAG_HOR_CENTERED) + aPos.AdjustX((rRect.GetWidth() - aImageSize.Width()) / 2 ); + if (nPaintFlags & PAINTFLAG_VER_CENTERED) + aPos.AdjustY((rRect.GetHeight() - aImageSize.Height()) / 2 ); + SvtIconChoiceCtrl::DrawEntryImage(pEntry, aPos, rRenderContext); + } +} + +void SvxIconChoiceCtrl_Impl::PaintEntry(SvxIconChoiceCtrlEntry* pEntry, const Point& rPos, vcl::RenderContext& rRenderContext) +{ + bool bSelected = false; + + if (eSelectionMode != SelectionMode::NONE) + bSelected = pEntry->IsSelected(); + + rRenderContext.Push(vcl::PushFlags::FONT | vcl::PushFlags::TEXTCOLOR); + + OUString aEntryText(SvtIconChoiceCtrl::GetEntryText(pEntry)); + tools::Rectangle aTextRect(CalcTextRect(pEntry, &rPos, &aEntryText)); + tools::Rectangle aBmpRect(CalcBmpRect(pEntry, &rPos)); + + bool bShowSelection = (bSelected && (eSelectionMode != SelectionMode::NONE)); + + bool bActiveSelection = (0 != (nWinBits & WB_NOHIDESELECTION)) || pView->HasFocus(); + + if (bShowSelection) + { + const StyleSettings& rSettings = rRenderContext.GetSettings().GetStyleSettings(); + vcl::Font aNewFont(rRenderContext.GetFont()); + + // font fill colors that are attributed "hard" need corresponding "hard" + // attributed highlight colors + if ((nWinBits & WB_NOHIDESELECTION) || pView->HasFocus()) + aNewFont.SetFillColor(rSettings.GetHighlightColor()); + else + aNewFont.SetFillColor(rSettings.GetDeactiveColor()); + + Color aWinCol = rSettings.GetWindowTextColor(); + if (!bActiveSelection && rSettings.GetFaceColor().IsBright() == aWinCol.IsBright()) + aNewFont.SetColor(rSettings.GetWindowTextColor()); + else + aNewFont.SetColor(rSettings.GetHighlightTextColor()); + + rRenderContext.SetFont(aNewFont); + + rRenderContext.SetFillColor(rRenderContext.GetBackground().GetColor()); + rRenderContext.DrawRect(CalcFocusRect(pEntry)); + rRenderContext.SetFillColor(); + } + + bool bResetClipRegion = false; + if (!rRenderContext.IsClipRegion() && (aVerSBar->IsVisible() || aHorSBar->IsVisible())) + { + tools::Rectangle aOutputArea(GetOutputRect()); + if (aOutputArea.Overlaps(aTextRect) || aOutputArea.Overlaps(aBmpRect)) + { + rRenderContext.SetClipRegion(vcl::Region(aOutputArea)); + bResetClipRegion = true; + } + } + + bool bLargeIconMode = WB_ICON == ( nWinBits & VIEWMODE_MASK ); + sal_uInt16 nBmpPaintFlags = PAINTFLAG_VER_CENTERED; + if (bLargeIconMode) + nBmpPaintFlags |= PAINTFLAG_HOR_CENTERED; + sal_uInt16 nTextPaintFlags = bLargeIconMode ? PAINTFLAG_HOR_CENTERED : PAINTFLAG_VER_CENTERED; + + PaintEmphasis(aTextRect, bSelected, rRenderContext); + + if ( bShowSelection ) + vcl::RenderTools::DrawSelectionBackground(rRenderContext, *pView, CalcFocusRect(pEntry), + bActiveSelection ? 1 : 2, false, true, false); + + + PaintItem(aBmpRect, IcnViewFieldType::Image, pEntry, nBmpPaintFlags, rRenderContext); + + PaintItem(aTextRect, IcnViewFieldType::Text, pEntry, nTextPaintFlags, rRenderContext); + + // draw highlight frame + if (pEntry == pCurHighlightFrame) + DrawHighlightFrame(rRenderContext, CalcFocusRect(pEntry)); + + rRenderContext.Pop(); + if (bResetClipRegion) + rRenderContext.SetClipRegion(); +} + +void SvxIconChoiceCtrl_Impl::SetEntryPos( SvxIconChoiceCtrlEntry* pEntry, const Point& rPos ) +{ + ShowCursor( false ); + tools::Rectangle aBoundRect( GetEntryBoundRect( pEntry )); + pView->Invalidate( aBoundRect ); + ToTop( pEntry ); + if( !IsAutoArrange() ) + { + bool bAdjustVirtSize = false; + if( rPos != aBoundRect.TopLeft() ) + { + Point aGridOffs( + pEntry->aGridRect.TopLeft() - pEntry->aRect.TopLeft() ); + pImpCursor->Clear(); + pGridMap->Clear(); + aBoundRect.SetPos( rPos ); + pEntry->aRect = aBoundRect; + pEntry->aGridRect.SetPos( rPos + aGridOffs ); + bAdjustVirtSize = true; + } + if( bAdjustVirtSize ) + AdjustVirtSize( pEntry->aRect ); + + pView->Invalidate( pEntry->aRect ); + pGridMap->OccupyGrids( pEntry ); + } + else + { + SvxIconChoiceCtrlEntry* pPrev = FindEntryPredecessor( pEntry, rPos ); + SetEntryPredecessor( pEntry, pPrev ); + aAutoArrangeIdle.Start(); + } + ShowCursor( true ); +} + +void SvxIconChoiceCtrl_Impl::SetNoSelection() +{ + // block recursive calls via SelectEntry + if( !(nFlags & IconChoiceFlags::ClearingSelection )) + { + nFlags |= IconChoiceFlags::ClearingSelection; + DeselectAllBut( nullptr ); + nFlags &= ~IconChoiceFlags::ClearingSelection; + } +} + +SvxIconChoiceCtrlEntry* SvxIconChoiceCtrl_Impl::GetEntry( const Point& rDocPos, bool bHit ) +{ + CheckBoundingRects(); + // search through z-order list from the end + size_t nCount = maZOrderList.size(); + while( nCount ) + { + nCount--; + SvxIconChoiceCtrlEntry* pEntry = maZOrderList[ nCount ]; + if( pEntry->aRect.Contains( rDocPos ) ) + { + if( bHit ) + { + tools::Rectangle aRect = CalcBmpRect( pEntry ); + aRect.AdjustTop( -3 ); + aRect.AdjustBottom(3 ); + aRect.AdjustLeft( -3 ); + aRect.AdjustRight(3 ); + if( aRect.Contains( rDocPos ) ) + return pEntry; + aRect = CalcTextRect( pEntry ); + if( aRect.Contains( rDocPos ) ) + return pEntry; + } + else + return pEntry; + } + } + return nullptr; +} + +void SvxIconChoiceCtrl_Impl::MakeEntryVisible( SvxIconChoiceCtrlEntry* pEntry, bool bBound ) +{ + if ( bBound ) + { + const tools::Rectangle& rRect = GetEntryBoundRect( pEntry ); + MakeVisible( rRect ); + } + else + { + tools::Rectangle aRect = CalcBmpRect( pEntry ); + aRect.Union( CalcTextRect( pEntry ) ); + aRect.AdjustTop(TBOFFS_BOUND ); + aRect.AdjustBottom(TBOFFS_BOUND ); + aRect.AdjustLeft(LROFFS_BOUND ); + aRect.AdjustRight(LROFFS_BOUND ); + MakeVisible( aRect ); + } +} + +const tools::Rectangle& SvxIconChoiceCtrl_Impl::GetEntryBoundRect( SvxIconChoiceCtrlEntry* pEntry ) +{ + if( !IsBoundingRectValid( pEntry->aRect )) + FindBoundingRect( pEntry ); + return pEntry->aRect; +} + +tools::Rectangle SvxIconChoiceCtrl_Impl::CalcBmpRect( SvxIconChoiceCtrlEntry* pEntry, const Point* pPos ) +{ + tools::Rectangle aBound = GetEntryBoundRect( pEntry ); + if( pPos ) + aBound.SetPos( *pPos ); + Point aPos( aBound.TopLeft() ); + + switch( nWinBits & VIEWMODE_MASK ) + { + case WB_ICON: + { + aPos.AdjustX(( aBound.GetWidth() - aImageSize.Width() ) / 2 ); + return tools::Rectangle( aPos, aImageSize ); + } + + case WB_SMALLICON: + case WB_DETAILS: + aPos.AdjustY(( aBound.GetHeight() - aImageSize.Height() ) / 2 ); + //TODO: determine horizontal distance to bounding rectangle + return tools::Rectangle( aPos, aImageSize ); + + default: + OSL_FAIL("IconView: Viewmode not set"); + return aBound; + } +} + +tools::Rectangle SvxIconChoiceCtrl_Impl::CalcTextRect( SvxIconChoiceCtrlEntry* pEntry, + const Point* pEntryPos, const OUString* pStr ) +{ + OUString aEntryText; + if( !pStr ) + aEntryText = SvtIconChoiceCtrl::GetEntryText( pEntry ); + else + aEntryText = *pStr; + + const tools::Rectangle aMaxTextRect( CalcMaxTextRect( pEntry ) ); + tools::Rectangle aBound( GetEntryBoundRect( pEntry ) ); + if( pEntryPos ) + aBound.SetPos( *pEntryPos ); + + tools::Rectangle aTextRect = pView->GetTextRect( aMaxTextRect, aEntryText, nCurTextDrawFlags ); + + Size aTextSize( aTextRect.GetSize() ); + + Point aPos( aBound.TopLeft() ); + tools::Long nBoundWidth = aBound.GetWidth(); + tools::Long nBoundHeight = aBound.GetHeight(); + + switch( nWinBits & VIEWMODE_MASK ) + { + case WB_ICON: + aPos.AdjustY(aImageSize.Height() ); + aPos.AdjustY(VER_DIST_BMP_STRING ); + aPos.AdjustX((nBoundWidth - aTextSize.Width()) / 2 ); + break; + + case WB_SMALLICON: + case WB_DETAILS: + aPos.AdjustX(aImageSize.Width() ); + aPos.AdjustX(HOR_DIST_BMP_STRING ); + aPos.AdjustY((nBoundHeight - aTextSize.Height()) / 2 ); + break; + } + return tools::Rectangle( aPos, aTextSize ); +} + + +tools::Long SvxIconChoiceCtrl_Impl::CalcBoundingWidth() const +{ + tools::Long nStringWidth = GetItemSize( IcnViewFieldType::Text ).Width(); + tools::Long nWidth = 0; + + switch( nWinBits & VIEWMODE_MASK ) + { + case WB_ICON: + nWidth = std::max( nStringWidth, aImageSize.Width() ); + break; + + case WB_SMALLICON: + case WB_DETAILS: + nWidth = aImageSize.Width(); + nWidth += HOR_DIST_BMP_STRING; + nWidth += nStringWidth; + break; + } + return nWidth; +} + +tools::Long SvxIconChoiceCtrl_Impl::CalcBoundingHeight() const +{ + tools::Long nStringHeight = GetItemSize(IcnViewFieldType::Text).Height(); + tools::Long nHeight = 0; + + switch( nWinBits & VIEWMODE_MASK ) + { + case WB_ICON: + nHeight = aImageSize.Height(); + nHeight += VER_DIST_BMP_STRING; + nHeight += nStringHeight; + break; + + case WB_SMALLICON: + case WB_DETAILS: + nHeight = std::max( aImageSize.Height(), nStringHeight ); + break; + } + if( nHeight > nMaxBoundHeight ) + { + const_cast<SvxIconChoiceCtrl_Impl*>(this)->nMaxBoundHeight = nHeight; + const_cast<SvxIconChoiceCtrl_Impl*>(this)->aHorSBar->SetLineSize( GetScrollBarLineSize() ); + const_cast<SvxIconChoiceCtrl_Impl*>(this)->aVerSBar->SetLineSize( GetScrollBarLineSize() ); + } + return nHeight; +} + +Size SvxIconChoiceCtrl_Impl::CalcBoundingSize() const +{ + return Size( CalcBoundingWidth(), CalcBoundingHeight() ); +} + +void SvxIconChoiceCtrl_Impl::RecalcAllBoundingRectsSmart() +{ + nMaxBoundHeight = 0; + maZOrderList.clear(); + size_t nCur; + SvxIconChoiceCtrlEntry* pEntry; + const size_t nCount = maEntries.size(); + + if( !IsAutoArrange() || !pHead ) + { + for( nCur = 0; nCur < nCount; nCur++ ) + { + pEntry = maEntries[ nCur ].get(); + if( IsBoundingRectValid( pEntry->aRect )) + { + Size aBoundSize( pEntry->aRect.GetSize() ); + if( aBoundSize.Height() > nMaxBoundHeight ) + nMaxBoundHeight = aBoundSize.Height(); + } + else + FindBoundingRect( pEntry ); + maZOrderList.push_back( pEntry ); + } + } + else + { + nCur = 0; + pEntry = pHead; + while( nCur != nCount ) + { + DBG_ASSERT(pEntry->pflink&&pEntry->pblink,"SvxIconChoiceCtrl_Impl::RecalcAllBoundingRect > Bad link(s)"); + if( IsBoundingRectValid( pEntry->aRect )) + { + Size aBoundSize( pEntry->aRect.GetSize() ); + if( aBoundSize.Height() > nMaxBoundHeight ) + nMaxBoundHeight = aBoundSize.Height(); + } + else + FindBoundingRect( pEntry ); + maZOrderList.push_back( pEntry ); + pEntry = pEntry->pflink; + nCur++; + } + } + AdjustScrollBars(); +} + +void SvxIconChoiceCtrl_Impl::FindBoundingRect( SvxIconChoiceCtrlEntry* pEntry ) +{ + DBG_ASSERT(!pEntry->IsPosLocked(),"Locked entry pos in FindBoundingRect"); + if( pEntry->IsPosLocked() && IsBoundingRectValid( pEntry->aRect) ) + { + AdjustVirtSize( pEntry->aRect ); + return; + } + Size aSize( CalcBoundingSize() ); + Point aPos(pGridMap->GetGridRect(pGridMap->GetUnoccupiedGrid()).TopLeft()); + SetBoundingRect_Impl( pEntry, aPos, aSize ); +} + +void SvxIconChoiceCtrl_Impl::SetBoundingRect_Impl( SvxIconChoiceCtrlEntry* pEntry, const Point& rPos, + const Size& /*rBoundingSize*/ ) +{ + tools::Rectangle aGridRect( rPos, Size(nGridDX, nGridDY) ); + pEntry->aGridRect = aGridRect; + Center( pEntry ); + AdjustVirtSize( pEntry->aRect ); + pGridMap->OccupyGrids( pEntry ); +} + + +void SvxIconChoiceCtrl_Impl::SetCursor( SvxIconChoiceCtrlEntry* pEntry ) +{ + if( pEntry == pCursor ) + { + if( pCursor && eSelectionMode == SelectionMode::Single && + !pCursor->IsSelected() ) + SelectEntry( pCursor, true ); + return; + } + ShowCursor( false ); + SvxIconChoiceCtrlEntry* pOldCursor = pCursor; + pCursor = pEntry; + if( pOldCursor ) + { + pOldCursor->ClearFlags( SvxIconViewFlags::FOCUSED ); + if( eSelectionMode == SelectionMode::Single ) + SelectEntry( pOldCursor, false ); // deselect old cursor + } + if( pCursor ) + { + ToTop( pCursor ); + pCursor->SetFlags( SvxIconViewFlags::FOCUSED ); + if( eSelectionMode == SelectionMode::Single ) + SelectEntry( pCursor, true ); + ShowCursor( true ); + } +} + + +void SvxIconChoiceCtrl_Impl::ShowCursor( bool bShow ) +{ + if( !pCursor || !bShow || !pView->HasFocus() ) + { + pView->HideFocus(); + return; + } + tools::Rectangle aRect ( CalcFocusRect( pCursor ) ); + /*pView->*/ShowFocus( aRect ); +} + + +void SvxIconChoiceCtrl_Impl::HideDDIcon() +{ + pView->PaintImmediately(); +} + +bool SvxIconChoiceCtrl_Impl::HandleScrollCommand( const CommandEvent& rCmd ) +{ + tools::Rectangle aDocRect( Point(), aVirtOutputSize ); + tools::Rectangle aVisRect( GetOutputRect() ); + if( aVisRect.Contains( aDocRect )) + return false; + Size aDocSize( aDocRect.GetSize() ); + Size aVisSize( aVisRect.GetSize() ); + bool bHor = aDocSize.Width() > aVisSize.Width(); + bool bVer = aDocSize.Height() > aVisSize.Height(); + + tools::Long nScrollDX = 0, nScrollDY = 0; + + switch( rCmd.GetCommand() ) + { + case CommandEventId::StartAutoScroll: + { + pView->EndTracking(); + StartAutoScrollFlags nScrollFlags = StartAutoScrollFlags::NONE; + if( bHor ) + nScrollFlags |= StartAutoScrollFlags::Horz; + if( bVer ) + nScrollFlags |= StartAutoScrollFlags::Vert; + if( nScrollFlags != StartAutoScrollFlags::NONE ) + { + pView->StartAutoScroll( nScrollFlags ); + return true; + } + } + break; + + case CommandEventId::Wheel: + { + const CommandWheelData* pData = rCmd.GetWheelData(); + if( pData && (CommandWheelMode::SCROLL == pData->GetMode()) && !pData->IsHorz() ) + { + sal_uLong nScrollLines = pData->GetScrollLines(); + if( nScrollLines == COMMAND_WHEEL_PAGESCROLL ) + { + nScrollDY = GetScrollBarPageSize( aVisSize.Width() ); + if( pData->GetDelta() < 0 ) + nScrollDY *= -1; + } + else + { + nScrollDY = pData->GetNotchDelta() * static_cast<tools::Long>(nScrollLines); + nScrollDY *= GetScrollBarLineSize(); + } + } + } + break; + + case CommandEventId::AutoScroll: + { + const CommandScrollData* pData = rCmd.GetAutoScrollData(); + if( pData ) + { + nScrollDX = pData->GetDeltaX() * GetScrollBarLineSize(); + nScrollDY = pData->GetDeltaY() * GetScrollBarLineSize(); + } + } + break; + + default: break; + } + + if( nScrollDX || nScrollDY ) + { + aVisRect.AdjustTop( -nScrollDY ); + aVisRect.AdjustBottom( -nScrollDY ); + aVisRect.AdjustLeft( -nScrollDX ); + aVisRect.AdjustRight( -nScrollDX ); + MakeVisible( aVisRect ); + return true; + } + return false; +} + + +void SvxIconChoiceCtrl_Impl::Command( const CommandEvent& rCEvt ) +{ + // scroll mouse event? + if( (rCEvt.GetCommand() == CommandEventId::Wheel) || + (rCEvt.GetCommand() == CommandEventId::StartAutoScroll) || + (rCEvt.GetCommand() == CommandEventId::AutoScroll) ) + { + if( HandleScrollCommand( rCEvt ) ) + return; + } +} + +void SvxIconChoiceCtrl_Impl::ToTop( SvxIconChoiceCtrlEntry* pEntry ) +{ + if( maZOrderList.empty() || pEntry == maZOrderList.back()) + return; + + auto it = std::find(maZOrderList.begin(), maZOrderList.end(), pEntry); + if (it != maZOrderList.end()) + { + maZOrderList.erase( it ); + maZOrderList.push_back( pEntry ); + } +} + +void SvxIconChoiceCtrl_Impl::ClipAtVirtOutRect( tools::Rectangle& rRect ) const +{ + if( rRect.Bottom() >= aVirtOutputSize.Height() ) + rRect.SetBottom( aVirtOutputSize.Height() - 1 ); + if( rRect.Right() >= aVirtOutputSize.Width() ) + rRect.SetRight( aVirtOutputSize.Width() - 1 ); + if( rRect.Top() < 0 ) + rRect.SetTop( 0 ); + if( rRect.Left() < 0 ) + rRect.SetLeft( 0 ); +} + +// rRect: area of the document (in document coordinates) that we want to make +// visible +// bScrBar == true: rectangle was calculated because of a scrollbar event + +void SvxIconChoiceCtrl_Impl::MakeVisible( const tools::Rectangle& rRect, bool bScrBar ) +{ + tools::Rectangle aVirtRect( rRect ); + ClipAtVirtOutRect( aVirtRect ); + Point aOrigin( pView->GetMapMode().GetOrigin() ); + // convert to document coordinate + aOrigin *= -1; + tools::Rectangle aOutputArea( GetOutputRect() ); + if( aOutputArea.Contains( aVirtRect ) ) + return; // is already visible + + tools::Long nDy; + if( aVirtRect.Top() < aOutputArea.Top() ) + { + // scroll up (nDy < 0) + nDy = aVirtRect.Top() - aOutputArea.Top(); + } + else if( aVirtRect.Bottom() > aOutputArea.Bottom() ) + { + // scroll down (nDy > 0) + nDy = aVirtRect.Bottom() - aOutputArea.Bottom(); + } + else + nDy = 0; + + tools::Long nDx; + if( aVirtRect.Left() < aOutputArea.Left() ) + { + // scroll to the left (nDx < 0) + nDx = aVirtRect.Left() - aOutputArea.Left(); + } + else if( aVirtRect.Right() > aOutputArea.Right() ) + { + // scroll to the right (nDx > 0) + nDx = aVirtRect.Right() - aOutputArea.Right(); + } + else + nDx = 0; + + aOrigin.AdjustX(nDx ); + aOrigin.AdjustY(nDy ); + aOutputArea.SetPos( aOrigin ); + if( GetUpdateMode() ) + { + HideDDIcon(); + pView->PaintImmediately(); + ShowCursor( false ); + } + + // invert origin for SV (so we can scroll/paint using document coordinates) + aOrigin *= -1; + SetOrigin( aOrigin ); + + bool bScrollable = pView->GetBackground().IsScrollable(); + + if( bScrollable && GetUpdateMode() ) + { + // scroll in reverse direction! + pView->Control::Scroll( -nDx, -nDy, aOutputArea, + ScrollFlags::NoChildren | ScrollFlags::UseClipRegion | ScrollFlags::Clip ); + } + else + pView->Invalidate(InvalidateFlags::NoChildren); + + if( aHorSBar->IsVisible() || aVerSBar->IsVisible() ) + { + if( !bScrBar ) + { + aOrigin *= -1; + // correct thumbs + if(aHorSBar->IsVisible() && aHorSBar->GetThumbPos() != aOrigin.X()) + aHorSBar->SetThumbPos( aOrigin.X() ); + if(aVerSBar->IsVisible() && aVerSBar->GetThumbPos() != aOrigin.Y()) + aVerSBar->SetThumbPos( aOrigin.Y() ); + } + } + + if( GetUpdateMode() ) + ShowCursor( true ); + + // check if we still need scrollbars + CheckScrollBars(); + if( bScrollable && GetUpdateMode() ) + pView->PaintImmediately(); + + // If the requested area can not be made completely visible, the + // Vis-Rect-Changed handler is called in any case. This case may occur e.g. + // if only few pixels of the lower border are invisible, but a scrollbar has + // a larger line size. + VisRectChanged(); +} + +sal_Int32 SvxIconChoiceCtrl_Impl::GetSelectionCount() const +{ + if( (nWinBits & WB_HIGHLIGHTFRAME) && pCurHighlightFrame ) + return 1; + return nSelectionCount; +} + +void SvxIconChoiceCtrl_Impl::ToggleSelection( SvxIconChoiceCtrlEntry* pEntry ) +{ + bool bSel; + bSel = !pEntry->IsSelected(); + SelectEntry( pEntry, bSel, true ); +} + +void SvxIconChoiceCtrl_Impl::DeselectAllBut( SvxIconChoiceCtrlEntry const * pThisEntryNot ) +{ + ClearSelectedRectList(); + + // TODO: work through z-order list, if necessary! + + size_t nCount = maEntries.size(); + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = maEntries[ nCur ].get(); + if( pEntry != pThisEntryNot && pEntry->IsSelected() ) + SelectEntry( pEntry, false, true ); + } + pAnchor = nullptr; + nFlags &= ~IconChoiceFlags::AddMode; +} + +Size SvxIconChoiceCtrl_Impl::GetMinGrid() const +{ + Size aMinSize( aImageSize ); + aMinSize.AdjustWidth(2 * LROFFS_BOUND ); + aMinSize.AdjustHeight(TBOFFS_BOUND ); // single offset is enough (FileDlg) + Size aTextSize( pView->GetTextWidth( "XXX" ), pView->GetTextHeight() ); + if( nWinBits & WB_ICON ) + { + aMinSize.AdjustHeight(VER_DIST_BMP_STRING ); + aMinSize.AdjustHeight(aTextSize.Height() ); + } + else + { + aMinSize.AdjustWidth(HOR_DIST_BMP_STRING ); + aMinSize.AdjustWidth(aTextSize.Width() ); + } + return aMinSize; +} + +void SvxIconChoiceCtrl_Impl::SetGrid( const Size& rSize ) +{ + Size aSize( rSize ); + Size aMinSize( GetMinGrid() ); + if( aSize.Width() < aMinSize.Width() ) + aSize.setWidth( aMinSize.Width() ); + if( aSize.Height() < aMinSize.Height() ) + aSize.setHeight( aMinSize.Height() ); + + nGridDX = aSize.Width(); + // HACK: Detail mode is not yet fully implemented, this workaround makes it + // fly with a single column + if( nWinBits & WB_DETAILS ) + { + const SvxIconChoiceCtrlColumnInfo* pCol = GetColumn( 0 ); + if( pCol ) + const_cast<SvxIconChoiceCtrlColumnInfo*>(pCol)->SetWidth( nGridDX ); + } + nGridDY = aSize.Height(); + SetDefaultTextSize(); +} + +// Calculates the maximum size that the text rectangle may use within its +// bounding rectangle. In WB_ICON mode with SvxIconChoiceCtrlTextMode::Full, Bottom is set to +// LONG_MAX. + +tools::Rectangle SvxIconChoiceCtrl_Impl::CalcMaxTextRect( const SvxIconChoiceCtrlEntry* pEntry ) const +{ + tools::Rectangle aBoundRect; + // avoid infinite recursion: don't calculate the bounding rectangle here + if( IsBoundingRectValid( pEntry->aRect ) ) + aBoundRect = pEntry->aRect; + else + aBoundRect = pEntry->aGridRect; + + tools::Rectangle aBmpRect( const_cast<SvxIconChoiceCtrl_Impl*>(this)->CalcBmpRect( + const_cast<SvxIconChoiceCtrlEntry*>(pEntry) ) ); + if( nWinBits & WB_ICON ) + { + aBoundRect.SetTop( aBmpRect.Bottom() ); + aBoundRect.AdjustTop(VER_DIST_BMP_STRING ); + if( aBoundRect.Top() > aBoundRect.Bottom()) + aBoundRect.SetTop( aBoundRect.Bottom() ); + aBoundRect.AdjustLeft(LROFFS_BOUND ); + aBoundRect.AdjustLeft( 1 ); + aBoundRect.AdjustRight( -(LROFFS_BOUND) ); + aBoundRect.AdjustRight( -1 ); + if( aBoundRect.Left() > aBoundRect.Right()) + aBoundRect.SetLeft( aBoundRect.Right() ); + if( pEntry->GetTextMode() == SvxIconChoiceCtrlTextMode::Full ) + aBoundRect.SetBottom( LONG_MAX ); + } + else + { + aBoundRect.SetLeft( aBmpRect.Right() ); + aBoundRect.AdjustLeft(HOR_DIST_BMP_STRING ); + aBoundRect.AdjustRight( -(LROFFS_BOUND) ); + if( aBoundRect.Left() > aBoundRect.Right() ) + aBoundRect.SetLeft( aBoundRect.Right() ); + tools::Long nHeight = aBoundRect.GetSize().Height(); + nHeight = nHeight - aDefaultTextSize.Height(); + nHeight /= 2; + aBoundRect.AdjustTop(nHeight ); + aBoundRect.AdjustBottom( -nHeight ); + } + return aBoundRect; +} + +void SvxIconChoiceCtrl_Impl::SetDefaultTextSize() +{ + tools::Long nDY = nGridDY; + nDY -= aImageSize.Height(); + nDY -= VER_DIST_BMP_STRING; + nDY -= 2 * TBOFFS_BOUND; + if (nDY <= 0) + nDY = 2; + + tools::Long nDX = nGridDX; + nDX -= 2 * LROFFS_BOUND; + nDX -= 2; + if (nDX <= 0) + nDX = 2; + + tools::Long nHeight = pView->GetTextHeight(); + if (nDY < nHeight) + nDY = nHeight; + if(pView->GetDPIScaleFactor() > 1) + { + nDY*=2; + } + aDefaultTextSize = Size(nDX, nDY); +} + + +void SvxIconChoiceCtrl_Impl::Center( SvxIconChoiceCtrlEntry* pEntry ) const +{ + pEntry->aRect = pEntry->aGridRect; + Size aSize( CalcBoundingSize() ); + if( nWinBits & WB_ICON ) + { + // center horizontally + tools::Long nBorder = pEntry->aGridRect.GetWidth() - aSize.Width(); + pEntry->aRect.AdjustLeft(nBorder / 2 ); + pEntry->aRect.AdjustRight( -(nBorder / 2) ); + } + // center vertically + pEntry->aRect.SetBottom( pEntry->aRect.Top() + aSize.Height() ); +} + + +// The deltas are the offsets by which the view is moved on the document. +// left, up: offsets < 0 +// right, down: offsets > 0 +void SvxIconChoiceCtrl_Impl::Scroll( tools::Long nDeltaX, tools::Long nDeltaY ) +{ + const MapMode& rMapMode = pView->GetMapMode(); + Point aOrigin( rMapMode.GetOrigin() ); + // convert to document coordinate + aOrigin *= -1; + aOrigin.AdjustY(nDeltaY ); + aOrigin.AdjustX(nDeltaX ); + tools::Rectangle aRect( aOrigin, aOutputSize ); + MakeVisible( aRect, true/*bScrollBar*/ ); +} + + +const Size& SvxIconChoiceCtrl_Impl::GetItemSize( IcnViewFieldType eItem ) const +{ + if (eItem == IcnViewFieldType::Text) + return aDefaultTextSize; + return aImageSize; // IcnViewFieldType::Image +} + +tools::Rectangle SvxIconChoiceCtrl_Impl::CalcFocusRect( SvxIconChoiceCtrlEntry* pEntry ) +{ + tools::Rectangle aTextRect( CalcTextRect( pEntry ) ); + tools::Rectangle aBoundRect( GetEntryBoundRect( pEntry ) ); + return tools::Rectangle( + aBoundRect.Left(), aBoundRect.Top() - 1, aBoundRect.Right() - 1, + aTextRect.Bottom() + 1); +} + +// the hot spot is the inner 50% of the rectangle +static tools::Rectangle GetHotSpot( const tools::Rectangle& rRect ) +{ + tools::Rectangle aResult( rRect ); + aResult.Justify(); + Size aSize( rRect.GetSize() ); + tools::Long nDelta = aSize.Width() / 4; + aResult.AdjustLeft(nDelta ); + aResult.AdjustRight( -nDelta ); + nDelta = aSize.Height() / 4; + aResult.AdjustTop(nDelta ); + aResult.AdjustBottom( -nDelta ); + return aResult; +} + +void SvxIconChoiceCtrl_Impl::SelectRect( SvxIconChoiceCtrlEntry* pEntry1, SvxIconChoiceCtrlEntry* pEntry2, + bool bAdd, std::vector<tools::Rectangle>* pOtherRects ) +{ + DBG_ASSERT(pEntry1 && pEntry2,"SelectEntry: Invalid Entry-Ptr"); + tools::Rectangle aRect( GetEntryBoundRect( pEntry1 ) ); + aRect.Union( GetEntryBoundRect( pEntry2 ) ); + SelectRect( aRect, bAdd, pOtherRects ); +} + +void SvxIconChoiceCtrl_Impl::SelectRect( const tools::Rectangle& rRect, bool bAdd, + std::vector<tools::Rectangle>* pOtherRects ) +{ + aCurSelectionRect = rRect; + if( maZOrderList.empty() ) + return; + + // set flag, so ToTop won't be called in Select + bool bAlreadySelectingRect(nFlags & IconChoiceFlags::SelectingRect); + nFlags |= IconChoiceFlags::SelectingRect; + + CheckBoundingRects(); + pView->PaintImmediately(); + const size_t nCount = maZOrderList.size(); + + tools::Rectangle aRect( rRect ); + aRect.Justify(); + bool bCalcOverlap = (bAdd && pOtherRects && !pOtherRects->empty()); + + bool bResetClipRegion = false; + if( !pView->GetOutDev()->IsClipRegion() ) + { + bResetClipRegion = true; + pView->GetOutDev()->SetClipRegion(vcl::Region(GetOutputRect())); + } + + for( size_t nPos = 0; nPos < nCount; nPos++ ) + { + SvxIconChoiceCtrlEntry* pEntry = maZOrderList[ nPos ]; + + if( !IsBoundingRectValid( pEntry->aRect )) + FindBoundingRect( pEntry ); + tools::Rectangle aBoundRect( GetHotSpot( pEntry->aRect ) ); + bool bSelected = pEntry->IsSelected(); + + bool bOverlaps; + if( bCalcOverlap ) + bOverlaps = IsOver( pOtherRects, aBoundRect ); + else + bOverlaps = false; + bool bOver = aRect.Overlaps( aBoundRect ); + + if( bOver && !bOverlaps ) + { + // is inside the new selection rectangle and outside of any old one + // => select + if( !bSelected ) + SelectEntry( pEntry, true, true ); + } + else if( !bAdd ) + { + // is outside of the selection rectangle + // => deselect + if( bSelected ) + SelectEntry( pEntry, false, true ); + } + else if (bOverlaps) + { + // The entry is inside an old (=>span multiple rectangles with Ctrl) + // selection rectangle. + + // There is still a bug here! The selection status of an entry in a + // previous rectangle has to be restored, if it was touched by the + // current selection rectangle but is not inside it any more. + // For simplicity's sake, let's assume that all entries in the old + // rectangles were correctly selected. It is wrong to just deselect + // the intersection. + // Possible solution: remember a snapshot of the selection before + // spanning the rectangle. + if( aBoundRect.Overlaps( rRect)) + { + // deselect intersection between old rectangles and current rectangle + if( bSelected ) + SelectEntry( pEntry, false, true ); + } + else + { + // select entry of an old rectangle + if( !bSelected ) + SelectEntry( pEntry, true, true ); + } + } + else if( !bOver && bSelected ) + { + // this entry is completely outside the rectangle => deselect it + SelectEntry( pEntry, false, true ); + } + } + + if( !bAlreadySelectingRect ) + nFlags &= ~IconChoiceFlags::SelectingRect; + + pView->PaintImmediately(); + if( bResetClipRegion ) + pView->GetOutDev()->SetClipRegion(); +} + +void SvxIconChoiceCtrl_Impl::SelectRange( + SvxIconChoiceCtrlEntry const * pStart, + SvxIconChoiceCtrlEntry const * pEnd, + bool bAdd ) +{ + sal_uLong nFront = GetEntryListPos( pStart ); + sal_uLong nBack = GetEntryListPos( pEnd ); + sal_uLong nFirst = std::min( nFront, nBack ); + sal_uLong nLast = std::max( nFront, nBack ); + sal_uLong i; + SvxIconChoiceCtrlEntry* pEntry; + + if ( ! bAdd ) + { + // deselect everything before the first entry if not in + // adding mode + for ( i=0; i<nFirst; i++ ) + { + pEntry = GetEntry( i ); + if( pEntry->IsSelected() ) + SelectEntry( pEntry, false, true ); + } + } + + // select everything between nFirst and nLast + for ( i=nFirst; i<=nLast; i++ ) + { + pEntry = GetEntry( i ); + if( ! pEntry->IsSelected() ) + SelectEntry( pEntry, true, true ); + } + + if ( ! bAdd ) + { + // deselect everything behind the last entry if not in + // adding mode + sal_uLong nEnd = GetEntryCount(); + for ( ; i<nEnd; i++ ) + { + pEntry = GetEntry( i ); + if( pEntry->IsSelected() ) + SelectEntry( pEntry, false, true ); + } + } +} + +bool SvxIconChoiceCtrl_Impl::IsOver( std::vector<tools::Rectangle>* pRectList, const tools::Rectangle& rBoundRect ) +{ + const sal_uInt16 nCount = pRectList->size(); + for( sal_uInt16 nCur = 0; nCur < nCount; nCur++ ) + { + tools::Rectangle& rRect = (*pRectList)[ nCur ]; + if( rBoundRect.Overlaps( rRect )) + return true; + } + return false; +} + +void SvxIconChoiceCtrl_Impl::AddSelectedRect( SvxIconChoiceCtrlEntry* pEntry1, + SvxIconChoiceCtrlEntry* pEntry2 ) +{ + DBG_ASSERT(pEntry1 && pEntry2,"SelectEntry: Invalid Entry-Ptr"); + tools::Rectangle aRect( GetEntryBoundRect( pEntry1 ) ); + aRect.Union( GetEntryBoundRect( pEntry2 ) ); + AddSelectedRect( aRect ); +} + +void SvxIconChoiceCtrl_Impl::AddSelectedRect( const tools::Rectangle& rRect ) +{ + tools::Rectangle newRect = rRect; + newRect.Justify(); + aSelectedRectList.push_back( newRect ); +} + +void SvxIconChoiceCtrl_Impl::ClearSelectedRectList() +{ + aSelectedRectList.clear(); +} + +IMPL_LINK_NOARG(SvxIconChoiceCtrl_Impl, AutoArrangeHdl, Timer *, void) +{ + aAutoArrangeIdle.Stop(); + Arrange( IsAutoArrange(), 0, 0 ); +} + +IMPL_LINK_NOARG(SvxIconChoiceCtrl_Impl, VisRectChangedHdl, Timer *, void) +{ + aVisRectChangedIdle.Stop(); +} + +IMPL_LINK_NOARG(SvxIconChoiceCtrl_Impl, DocRectChangedHdl, Timer *, void) +{ + aDocRectChangedIdle.Stop(); +} + +#ifdef DBG_UTIL +void SvxIconChoiceCtrl_Impl::SetEntryTextMode( SvxIconChoiceCtrlTextMode eMode, SvxIconChoiceCtrlEntry* pEntry ) +{ + if( !pEntry ) + { + if( eTextMode != eMode ) + { + eTextMode = eMode; + Arrange( true, 0, 0 ); + } + } + else + { + if( pEntry->eTextMode != eMode ) + { + pEntry->eTextMode = eMode; + InvalidateEntry( pEntry ); + pView->Invalidate( GetEntryBoundRect( pEntry ) ); + AdjustVirtSize( pEntry->aRect ); + } + } +} +#endif + +// Draw my own focusrect, because the focusrect of the outputdevice has got the inverted color +// of the background. But what will we see, if the backgroundcolor is gray ? - We will see +// a gray focusrect on a gray background !!! + +void SvxIconChoiceCtrl_Impl::ShowFocus ( tools::Rectangle const & rRect ) +{ + Color aBkgColor(pView->GetBackground().GetColor()); + Color aPenColor; + sal_uInt16 nColor = ( aBkgColor.GetRed() + aBkgColor.GetGreen() + aBkgColor.GetBlue() ) / 3; + if (nColor > 128) + aPenColor = COL_BLACK; + else + aPenColor = COL_WHITE; + + aFocus.aPenColor = aPenColor; + aFocus.aRect = rRect; +} + +void SvxIconChoiceCtrl_Impl::DrawFocusRect(vcl::RenderContext& rRenderContext) +{ + rRenderContext.SetLineColor(aFocus.aPenColor); + rRenderContext.SetFillColor(); + tools::Polygon aPolygon (aFocus.aRect); + + LineInfo aLineInfo(LineStyle::Dash); + + aLineInfo.SetDashLen(1); + aLineInfo.SetDotLen(1); + aLineInfo.SetDistance(1); + aLineInfo.SetDotCount(1); + + rRenderContext.DrawPolyLine(aPolygon, aLineInfo); +} + +bool SvxIconChoiceCtrl_Impl::IsMnemonicChar( sal_Unicode cChar, sal_uLong& rPos ) const +{ + bool bRet = false; + const vcl::I18nHelper& rI18nHelper = Application::GetSettings().GetUILocaleI18nHelper(); + size_t nEntryCount = GetEntryCount(); + for ( size_t i = 0; i < nEntryCount; ++i ) + { + if ( rI18nHelper.MatchMnemonic( GetEntry( i )->GetText(), cChar ) ) + { + bRet = true; + rPos = i; + break; + } + } + + return bRet; +} + + +IMPL_LINK(SvxIconChoiceCtrl_Impl, UserEventHdl, void*, nId, void ) +{ + if( nId == EVENTID_ADJUST_SCROLLBARS ) + { + nUserEventAdjustScrBars = nullptr; + AdjustScrollBars(); + } + else if( nId == EVENTID_SHOW_CURSOR ) + { + ShowCursor( true ); + } +} + +void SvxIconChoiceCtrl_Impl::CancelUserEvents() +{ + if( nUserEventAdjustScrBars ) + { + Application::RemoveUserEvent( nUserEventAdjustScrBars ); + nUserEventAdjustScrBars = nullptr; + } +} + +void SvxIconChoiceCtrl_Impl::InvalidateEntry( SvxIconChoiceCtrlEntry* pEntry ) +{ + if( pEntry == pCursor ) + ShowCursor( false ); + pView->Invalidate( pEntry->aRect ); + Center( pEntry ); + pView->Invalidate( pEntry->aRect ); + if( pEntry == pCursor ) + ShowCursor( true ); +} + +SvxIconChoiceCtrlEntry* SvxIconChoiceCtrl_Impl::GetFirstSelectedEntry() const +{ + if( !GetSelectionCount() ) + return nullptr; + + if( (nWinBits & WB_HIGHLIGHTFRAME) && (eSelectionMode == SelectionMode::NONE) ) + { + return pCurHighlightFrame; + } + + size_t nCount = maEntries.size(); + if( !pHead ) + { + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = maEntries[ nCur ].get(); + if( pEntry->IsSelected() ) + { + return pEntry; + } + } + } + else + { + SvxIconChoiceCtrlEntry* pEntry = pHead; + while( nCount-- ) + { + if( pEntry->IsSelected() ) + { + return pEntry; + } + pEntry = pEntry->pflink; + if( nCount && pEntry == pHead ) + { + OSL_FAIL("SvxIconChoiceCtrl_Impl::GetFirstSelectedEntry > infinite loop!"); + return nullptr; + } + } + } + return nullptr; +} + +void SvxIconChoiceCtrl_Impl::SelectAll() +{ + size_t nCount = maEntries.size(); + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = maEntries[ nCur ].get(); + SelectEntry( pEntry, true/*bSelect*/, true ); + } + nFlags &= ~IconChoiceFlags::AddMode; + pAnchor = nullptr; +} + + + + +sal_Int32 SvxIconChoiceCtrl_Impl::GetEntryListPos( SvxIconChoiceCtrlEntry const * pEntry ) const +{ + if( !(nFlags & IconChoiceFlags::EntryListPosValid )) + const_cast<SvxIconChoiceCtrl_Impl*>(this)->SetListPositions(); + return pEntry->nPos; +} + +void SvxIconChoiceCtrl_Impl::InitSettings() +{ + const StyleSettings& rStyleSettings = pView->GetSettings().GetStyleSettings(); + + // unit (from settings) is Point + vcl::Font aFont( rStyleSettings.GetFieldFont() ); + aFont.SetColor( rStyleSettings.GetWindowTextColor() ); + pView->SetPointFont( aFont ); + SetDefaultTextSize(); + + pView->SetTextColor( rStyleSettings.GetFieldTextColor() ); + pView->SetTextFillColor(); + + pView->SetBackground( rStyleSettings.GetFieldColor()); + + tools::Long nScrBarSize = rStyleSettings.GetScrollBarSize(); + if( nScrBarSize == nHorSBarHeight && nScrBarSize == nVerSBarWidth ) + return; + + nHorSBarHeight = nScrBarSize; + Size aSize( aHorSBar->GetSizePixel() ); + aSize.setHeight( nScrBarSize ); + aHorSBar->Hide(); + aHorSBar->SetSizePixel( aSize ); + + nVerSBarWidth = nScrBarSize; + aSize = aVerSBar->GetSizePixel(); + aSize.setWidth( nScrBarSize ); + aVerSBar->Hide(); + aVerSBar->SetSizePixel( aSize ); + + Size aOSize( pView->Control::GetOutputSizePixel() ); + PositionScrollBars( aOSize.Width(), aOSize.Height() ); + AdjustScrollBars(); +} + +void SvxIconChoiceCtrl_Impl::SetPositionMode( SvxIconChoiceCtrlPositionMode eMode ) +{ + if( eMode == ePositionMode ) + return; + + SvxIconChoiceCtrlPositionMode eOldMode = ePositionMode; + ePositionMode = eMode; + size_t nCount = maEntries.size(); + + if( eOldMode == SvxIconChoiceCtrlPositionMode::AutoArrange ) + { + // when positioning moved entries "hard", there are problems with + // unwanted overlaps, as these entries aren't taken into account in + // Arrange. + if( maEntries.size() ) + aAutoArrangeIdle.Start(); + return; + } + + if( ePositionMode == SvxIconChoiceCtrlPositionMode::AutoArrange ) + { + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = maEntries[ nCur ].get(); + if( pEntry->GetFlags() & SvxIconViewFlags(SvxIconViewFlags::POS_LOCKED | SvxIconViewFlags::POS_MOVED)) + SetEntryPos(pEntry, GetEntryBoundRect( pEntry ).TopLeft()); + } + + if( maEntries.size() ) + aAutoArrangeIdle.Start(); + } +} + +void SvxIconChoiceCtrl_Impl::SetEntryPredecessor( SvxIconChoiceCtrlEntry* pEntry, + SvxIconChoiceCtrlEntry* pPredecessor ) +{ + if( !IsAutoArrange() ) + return; + + if( pEntry == pPredecessor ) + return; + + sal_uLong nPos1 = GetEntryListPos( pEntry ); + if( !pHead ) + { + if( pPredecessor ) + { + sal_uLong nPos2 = GetEntryListPos( pPredecessor ); + if( nPos1 == (nPos2 + 1) ) + return; // is already the predecessor + } + else if( !nPos1 ) + return; + + InitPredecessors(); + } + + if( !pPredecessor && pHead == pEntry ) + return; // is already the first one + + bool bSetHead = false; + if( !pPredecessor ) + { + bSetHead = true; + pPredecessor = pHead->pblink; + } + if( pEntry == pHead ) + { + pHead = pHead->pflink; + bSetHead = false; + } + if( pEntry != pPredecessor ) + { + pEntry->Unlink(); + pEntry->SetBacklink( pPredecessor ); + } + if( bSetHead ) + pHead = pEntry; + aAutoArrangeIdle.Start(); +} + +SvxIconChoiceCtrlEntry* SvxIconChoiceCtrl_Impl::FindEntryPredecessor( SvxIconChoiceCtrlEntry* pEntry, + const Point& rPosTopLeft ) +{ + Point aPos( rPosTopLeft ); //TopLeft + tools::Rectangle aCenterRect( CalcBmpRect( pEntry, &aPos )); + Point aNewPos( aCenterRect.Center() ); + GridId nGrid = GetPredecessorGrid( aNewPos ); + size_t nCount = maEntries.size(); + if( nGrid == GRID_NOT_FOUND ) + return nullptr; + if( nGrid >= nCount ) + nGrid = nCount - 1; + if( !pHead ) + return maEntries[ nGrid ].get(); + + SvxIconChoiceCtrlEntry* pCur = pHead; // Grid 0 + // TODO: go through list from the end if nGrid > nCount/2 + for( GridId nCur = 0; nCur < nGrid; nCur++ ) + pCur = pCur->pflink; + + return pCur; +} + +GridId SvxIconChoiceCtrl_Impl::GetPredecessorGrid( const Point& rPos) const +{ + Point aPos( rPos ); + aPos.AdjustX( -(LROFFS_WINBORDER) ); + aPos.AdjustY( -(TBOFFS_WINBORDER) ); + tools::Long nMaxCol = aVirtOutputSize.Width() / nGridDX; + if( nMaxCol ) + nMaxCol--; + tools::Long nGridX = aPos.X() / nGridDX; + if( nGridX > nMaxCol ) + nGridX = nMaxCol; + tools::Long nGridY = aPos.Y() / nGridDY; + tools::Long nGridsX = aOutputSize.Width() / nGridDX; + GridId nGrid = (nGridY * nGridsX) + nGridX; + tools::Long nMiddle = (nGridX * nGridDX) + (nGridDX / 2); + if( rPos.X() < nMiddle ) + { + if( !nGrid ) + nGrid = GRID_NOT_FOUND; + else + nGrid--; + } + return nGrid; +} + +bool SvxIconChoiceCtrl_Impl::RequestHelp( const HelpEvent& rHEvt ) +{ + if ( !(rHEvt.GetMode() & HelpEventMode::QUICK ) ) + return false; + + Point aPos( pView->ScreenToOutputPixel(rHEvt.GetMousePosPixel() ) ); + aPos -= pView->GetMapMode().GetOrigin(); + SvxIconChoiceCtrlEntry* pEntry = GetEntry( aPos, true ); + + if ( !pEntry ) + return false; + + OUString sQuickHelpText = pEntry->GetQuickHelpText(); + OUString aEntryText( SvtIconChoiceCtrl::GetEntryText( pEntry ) ); + tools::Rectangle aTextRect( CalcTextRect( pEntry, nullptr, &aEntryText ) ); + if ( ( !aTextRect.Contains( aPos ) || aEntryText.isEmpty() ) && sQuickHelpText.isEmpty() ) + return false; + + tools::Rectangle aOptTextRect( aTextRect ); + aOptTextRect.SetBottom( LONG_MAX ); + DrawTextFlags nNewFlags = nCurTextDrawFlags; + nNewFlags &= ~DrawTextFlags( DrawTextFlags::Clip | DrawTextFlags::EndEllipsis ); + aOptTextRect = pView->GetTextRect( aOptTextRect, aEntryText, nNewFlags ); + if ( aOptTextRect != aTextRect || !sQuickHelpText.isEmpty() ) + { + //aTextRect.Right() = aTextRect.Left() + aRealSize.Width() + 4; + Point aPt( aOptTextRect.TopLeft() ); + aPt += pView->GetMapMode().GetOrigin(); + aPt = pView->OutputToScreenPixel( aPt ); + // subtract border of tooltip help + aPt.AdjustY( -1 ); + aPt.AdjustX( -3 ); + aOptTextRect.SetPos( aPt ); + OUString sHelpText; + if ( !sQuickHelpText.isEmpty() ) + sHelpText = sQuickHelpText; + else + sHelpText = aEntryText; + Help::ShowQuickHelp( static_cast<vcl::Window*>(pView), aOptTextRect, sHelpText, QuickHelpFlags::Left | QuickHelpFlags::VCenter ); + } + + return true; +} + +void SvxIconChoiceCtrl_Impl::SetColumn( sal_uInt16 nIndex, const SvxIconChoiceCtrlColumnInfo& rInfo) +{ + if (!m_pColumns) + m_pColumns.reset(new SvxIconChoiceCtrlColumnInfoMap); + + SvxIconChoiceCtrlColumnInfo* pInfo = new SvxIconChoiceCtrlColumnInfo( rInfo ); + m_pColumns->insert(std::make_pair(nIndex, std::unique_ptr<SvxIconChoiceCtrlColumnInfo>(pInfo))); + + // HACK: Detail mode is not yet fully implemented, this workaround makes it + // fly with a single column + if( !nIndex && (nWinBits & WB_DETAILS) ) + nGridDX = pInfo->GetWidth(); + + if( GetUpdateMode() ) + Arrange( IsAutoArrange(), 0, 0 ); +} + +const SvxIconChoiceCtrlColumnInfo* SvxIconChoiceCtrl_Impl::GetColumn( sal_uInt16 nIndex ) const +{ + if (!m_pColumns) + return nullptr; + auto const it = m_pColumns->find( nIndex ); + if (it == m_pColumns->end()) + return nullptr; + return it->second.get(); +} + +void SvxIconChoiceCtrl_Impl::DrawHighlightFrame(vcl::RenderContext& rRenderContext, const tools::Rectangle& rBmpRect) +{ + tools::Rectangle aBmpRect(rBmpRect); + tools::Long nBorder = 2; + if (aImageSize.Width() < 32) + nBorder = 1; + aBmpRect.AdjustRight(nBorder ); + aBmpRect.AdjustLeft( -nBorder ); + aBmpRect.AdjustBottom(nBorder ); + aBmpRect.AdjustTop( -nBorder ); + + DecorationView aDecoView(&rRenderContext); + DrawHighlightFrameStyle nDecoFlags; + if (bHighlightFramePressed) + nDecoFlags = DrawHighlightFrameStyle::In; + else + nDecoFlags = DrawHighlightFrameStyle::Out; + aDecoView.DrawHighlightFrame(aBmpRect, nDecoFlags); +} + +void SvxIconChoiceCtrl_Impl::SetEntryHighlightFrame( SvxIconChoiceCtrlEntry* pEntry, + bool bKeepHighlightFlags ) +{ + if( pEntry == pCurHighlightFrame ) + return; + + if( !bKeepHighlightFlags ) + bHighlightFramePressed = false; + + if (pCurHighlightFrame) + { + tools::Rectangle aInvalidationRect(GetEntryBoundRect(pCurHighlightFrame)); + aInvalidationRect.expand(5); + pCurHighlightFrame = nullptr; + pView->Invalidate(aInvalidationRect); + } + + pCurHighlightFrame = pEntry; + if (pEntry) + { + tools::Rectangle aInvalidationRect(GetEntryBoundRect(pEntry)); + aInvalidationRect.expand(5); + pView->Invalidate(aInvalidationRect); + } +} + +void SvxIconChoiceCtrl_Impl::CallSelectHandler() +{ + // When single-click mode is active, the selection handler should be called + // synchronously, as the selection is automatically taken away once the + // mouse cursor doesn't touch the object any more. Else, we might run into + // missing calls to Select if the object is selected from a mouse movement, + // because when starting the timer, the mouse cursor might have already left + // the object. + // In special cases (=>SfxFileDialog!), synchronous calls can be forced via + // WB_NOASYNCSELECTHDL. + if( nWinBits & (WB_NOASYNCSELECTHDL | WB_HIGHLIGHTFRAME) ) + { + pHdlEntry = nullptr; + pView->ClickIcon(); + //pView->Select(); + } + else + aCallSelectHdlIdle.Start(); +} + +IMPL_LINK_NOARG(SvxIconChoiceCtrl_Impl, CallSelectHdlHdl, Timer *, void) +{ + pHdlEntry = nullptr; + pView->ClickIcon(); + //pView->Select(); +} + +void SvxIconChoiceCtrl_Impl::SetOrigin( const Point& rPos ) +{ + MapMode aMapMode( pView->GetMapMode() ); + aMapMode.SetOrigin( rPos ); + pView->SetMapMode( aMapMode ); +} + +void SvxIconChoiceCtrl_Impl::CallEventListeners( VclEventId nEvent, void* pData ) +{ + pView->CallImplEventListeners( nEvent, pData ); +} + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/imivctl2.cxx b/vcl/source/control/imivctl2.cxx new file mode 100644 index 000000000..5b0d2d8ad --- /dev/null +++ b/vcl/source/control/imivctl2.cxx @@ -0,0 +1,715 @@ +/* -*- 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 "imivctl.hxx" +#include <sal/log.hxx> + +IcnCursor_Impl::IcnCursor_Impl( SvxIconChoiceCtrl_Impl* pOwner ) +{ + pView = pOwner; + pCurEntry = nullptr; + nDeltaWidth = 0; + nDeltaHeight= 0; + nCols = 0; + nRows = 0; +} + +IcnCursor_Impl::~IcnCursor_Impl() +{ +} + +sal_uInt16 IcnCursor_Impl::GetSortListPos( SvxIconChoiceCtrlEntryPtrVec& rList, tools::Long nValue, + bool bVertical ) +{ + sal_uInt16 nCount = rList.size(); + if( !nCount ) + return 0; + + sal_uInt16 nCurPos = 0; + tools::Long nPrevValue = LONG_MIN; + while( nCount ) + { + const tools::Rectangle& rRect = pView->GetEntryBoundRect( rList[nCurPos] ); + tools::Long nCurValue; + if( bVertical ) + nCurValue = rRect.Top(); + else + nCurValue = rRect.Left(); + if( nValue >= nPrevValue && nValue <= nCurValue ) + return nCurPos; + nPrevValue = nCurValue; + nCount--; + nCurPos++; + } + return rList.size(); +} + +void IcnCursor_Impl::ImplCreate() +{ + pView->CheckBoundingRects(); + DBG_ASSERT(xColumns==nullptr&&xRows==nullptr,"ImplCreate: Not cleared"); + + SetDeltas(); + + xColumns.reset(new IconChoiceMap); + xRows.reset(new IconChoiceMap); + + size_t nCount = pView->maEntries.size(); + for( size_t nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = pView->maEntries[ nCur ].get(); + // const Rectangle& rRect = pView->GetEntryBoundRect( pEntry ); + tools::Rectangle rRect( pView->CalcBmpRect( pEntry ) ); + short nY = static_cast<short>( ((rRect.Top()+rRect.Bottom())/2) / nDeltaHeight ); + short nX = static_cast<short>( ((rRect.Left()+rRect.Right())/2) / nDeltaWidth ); + + // capture rounding errors + if( nY >= nRows ) + nY = sal::static_int_cast< short >(nRows - 1); + if( nX >= nCols ) + nX = sal::static_int_cast< short >(nCols - 1); + + SvxIconChoiceCtrlEntryPtrVec& rColEntry = (*xColumns)[nX]; + sal_uInt16 nIns = GetSortListPos( rColEntry, rRect.Top(), true ); + rColEntry.insert( rColEntry.begin() + nIns, pEntry ); + + SvxIconChoiceCtrlEntryPtrVec& rRowEntry = (*xRows)[nY]; + nIns = GetSortListPos( rRowEntry, rRect.Left(), false ); + rRowEntry.insert( rRowEntry.begin() + nIns, pEntry ); + + pEntry->nX = nX; + pEntry->nY = nY; + } +} + + +void IcnCursor_Impl::Clear() +{ + if( xColumns ) + { + xColumns.reset(); + xRows.reset(); + pCurEntry = nullptr; + nDeltaWidth = 0; + nDeltaHeight = 0; + } +} + +SvxIconChoiceCtrlEntry* IcnCursor_Impl::SearchCol(sal_uInt16 nCol, sal_uInt16 nTop, sal_uInt16 nBottom, + bool bDown, bool bSimple ) +{ + DBG_ASSERT(pCurEntry, "SearchCol: No reference entry"); + IconChoiceMap::iterator mapIt = xColumns->find( nCol ); + if ( mapIt == xColumns->end() ) + return nullptr; + SvxIconChoiceCtrlEntryPtrVec const & rList = mapIt->second; + const sal_uInt16 nCount = rList.size(); + if( !nCount ) + return nullptr; + + const tools::Rectangle& rRefRect = pView->GetEntryBoundRect(pCurEntry); + + if( bSimple ) + { + SvxIconChoiceCtrlEntryPtrVec::const_iterator it = std::find( rList.begin(), rList.end(), pCurEntry ); + + assert(it != rList.end()); //Entry not in Col-List + if (it == rList.end()) + return nullptr; + + if( bDown ) + { + while( ++it != rList.end() ) + { + SvxIconChoiceCtrlEntry* pEntry = *it; + const tools::Rectangle& rRect = pView->GetEntryBoundRect( pEntry ); + if( rRect.Top() > rRefRect.Top() ) + return pEntry; + } + return nullptr; + } + else + { + SvxIconChoiceCtrlEntryPtrVec::const_reverse_iterator it2(it); + while (it2 != rList.rend()) + { + SvxIconChoiceCtrlEntry* pEntry = *it2; + const tools::Rectangle& rRect = pView->GetEntryBoundRect( pEntry ); + if( rRect.Top() < rRefRect.Top() ) + return pEntry; + ++it2; + } + return nullptr; + } + } + + if( nTop > nBottom ) + std::swap(nTop, nBottom); + + tools::Long nMinDistance = LONG_MAX; + SvxIconChoiceCtrlEntry* pResult = nullptr; + for( sal_uInt16 nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = rList[ nCur ]; + if( pEntry != pCurEntry ) + { + sal_uInt16 nY = pEntry->nY; + if( nY >= nTop && nY <= nBottom ) + { + const tools::Rectangle& rRect = pView->GetEntryBoundRect( pEntry ); + tools::Long nDistance = rRect.Top() - rRefRect.Top(); + if( nDistance < 0 ) + nDistance *= -1; + if( nDistance && nDistance < nMinDistance ) + { + nMinDistance = nDistance; + pResult = pEntry; + } + } + } + } + return pResult; +} + +SvxIconChoiceCtrlEntry* IcnCursor_Impl::SearchRow(sal_uInt16 nRow, sal_uInt16 nLeft, sal_uInt16 nRight, + bool bRight, bool bSimple ) +{ + DBG_ASSERT(pCurEntry,"SearchRow: No reference entry"); + IconChoiceMap::iterator mapIt = xRows->find( nRow ); + if ( mapIt == xRows->end() ) + return nullptr; + SvxIconChoiceCtrlEntryPtrVec const & rList = mapIt->second; + const sal_uInt16 nCount = rList.size(); + if( !nCount ) + return nullptr; + + const tools::Rectangle& rRefRect = pView->GetEntryBoundRect(pCurEntry); + + if( bSimple ) + { + SvxIconChoiceCtrlEntryPtrVec::const_iterator it = std::find( rList.begin(), rList.end(), pCurEntry ); + + assert(it != rList.end()); //Entry not in Row-List + if (it == rList.end()) + return nullptr; + + if( bRight ) + { + while( ++it != rList.end() ) + { + SvxIconChoiceCtrlEntry* pEntry = *it; + const tools::Rectangle& rRect = pView->GetEntryBoundRect( pEntry ); + if( rRect.Left() > rRefRect.Left() ) + return pEntry; + } + return nullptr; + } + else + { + SvxIconChoiceCtrlEntryPtrVec::const_reverse_iterator it2(it); + while (it2 != rList.rend()) + { + SvxIconChoiceCtrlEntry* pEntry = *it2; + const tools::Rectangle& rRect = pView->GetEntryBoundRect( pEntry ); + if( rRect.Left() < rRefRect.Left() ) + return pEntry; + ++it2; + } + return nullptr; + } + + } + if( nRight < nLeft ) + std::swap(nRight, nLeft); + + tools::Long nMinDistance = LONG_MAX; + SvxIconChoiceCtrlEntry* pResult = nullptr; + for( sal_uInt16 nCur = 0; nCur < nCount; nCur++ ) + { + SvxIconChoiceCtrlEntry* pEntry = rList[ nCur ]; + if( pEntry != pCurEntry ) + { + sal_uInt16 nX = pEntry->nX; + if( nX >= nLeft && nX <= nRight ) + { + const tools::Rectangle& rRect = pView->GetEntryBoundRect( pEntry ); + tools::Long nDistance = rRect.Left() - rRefRect.Left(); + if( nDistance < 0 ) + nDistance *= -1; + if( nDistance && nDistance < nMinDistance ) + { + nMinDistance = nDistance; + pResult = pEntry; + } + } + } + } + return pResult; +} + + +/* + Searches, starting from the passed value, the next entry to the left/to the + right. Example for bRight = sal_True: + + c + b c + a b c + S 1 1 1 ====> search direction + a b c + b c + c + + S : starting position + 1 : first searched rectangle + a,b,c : 2nd, 3rd, 4th searched rectangle +*/ + +SvxIconChoiceCtrlEntry* IcnCursor_Impl::GoLeftRight( SvxIconChoiceCtrlEntry* pCtrlEntry, bool bRight ) +{ + SvxIconChoiceCtrlEntry* pResult; + pCurEntry = pCtrlEntry; + Create(); + sal_uInt16 nY = pCtrlEntry->nY; + sal_uInt16 nX = pCtrlEntry->nX; + DBG_ASSERT(nY< nRows,"GoLeftRight:Bad column"); + DBG_ASSERT(nX< nCols,"GoLeftRight:Bad row"); + // neighbor in same row? + if( bRight ) + pResult = SearchRow( + nY, nX, sal::static_int_cast< sal_uInt16 >(nCols-1), true, true ); + else + pResult = SearchRow( nY, 0, nX, false, true ); + if( pResult ) + return pResult; + + tools::Long nCurCol = nX; + + tools::Long nColOffs, nLastCol; + if( bRight ) + { + nColOffs = 1; + nLastCol = nCols; + } + else + { + nColOffs = -1; + nLastCol = -1; // 0-1 + } + + sal_uInt16 nRowMin = nY; + sal_uInt16 nRowMax = nY; + do + { + SvxIconChoiceCtrlEntry* pEntry = SearchCol(static_cast<sal_uInt16>(nCurCol), nRowMin, nRowMax, true, false); + if( pEntry ) + return pEntry; + if( nRowMin ) + nRowMin--; + if( nRowMax < (nRows-1)) + nRowMax++; + nCurCol += nColOffs; + } while( nCurCol != nLastCol ); + return nullptr; +} + +SvxIconChoiceCtrlEntry* IcnCursor_Impl::GoPageUpDown( SvxIconChoiceCtrlEntry* pStart, bool bDown) +{ + if( pView->IsAutoArrange() && !(pView->nWinBits & WB_ALIGN_TOP) ) + { + const tools::Long nPos = static_cast<tools::Long>(pView->GetEntryListPos( pStart )); + tools::Long nEntriesInView = pView->aOutputSize.Height() / pView->nGridDY; + nEntriesInView *= + ((pView->aOutputSize.Width()+(pView->nGridDX/2)) / pView->nGridDX ); + tools::Long nNewPos = nPos; + if( bDown ) + { + nNewPos += nEntriesInView; + if( nNewPos >= static_cast<tools::Long>(pView->maEntries.size()) ) + nNewPos = pView->maEntries.size() - 1; + } + else + { + nNewPos -= nEntriesInView; + if( nNewPos < 0 ) + nNewPos = 0; + } + if( nPos != nNewPos ) + return pView->maEntries[ static_cast<size_t>(nNewPos) ].get(); + return nullptr; + } + tools::Long nOpt = pView->GetEntryBoundRect( pStart ).Top(); + if( bDown ) + { + nOpt += pView->aOutputSize.Height(); + nOpt -= pView->nGridDY; + } + else + { + nOpt -= pView->aOutputSize.Height(); + nOpt += pView->nGridDY; + } + if( nOpt < 0 ) + nOpt = 0; + + tools::Long nPrevErr = LONG_MAX; + + SvxIconChoiceCtrlEntry* pPrev = pStart; + SvxIconChoiceCtrlEntry* pNext = GoUpDown( pStart, bDown ); + while( pNext ) + { + tools::Long nCur = pView->GetEntryBoundRect( pNext ).Top(); + tools::Long nErr = nOpt - nCur; + if( nErr < 0 ) + nErr *= -1; + if( nErr > nPrevErr ) + return pPrev; + nPrevErr = nErr; + pPrev = pNext; + pNext = GoUpDown( pNext, bDown ); + } + if( pPrev != pStart ) + return pPrev; + return nullptr; +} + +SvxIconChoiceCtrlEntry* IcnCursor_Impl::GoUpDown( SvxIconChoiceCtrlEntry* pCtrlEntry, bool bDown) +{ + if( pView->IsAutoArrange() && !(pView->nWinBits & WB_ALIGN_TOP) ) + { + sal_uLong nPos = pView->GetEntryListPos( pCtrlEntry ); + if( bDown && nPos < (pView->maEntries.size() - 1) ) + return pView->maEntries[ nPos + 1 ].get(); + else if( !bDown && nPos > 0 ) + return pView->maEntries[ nPos - 1 ].get(); + return nullptr; + } + + SvxIconChoiceCtrlEntry* pResult; + pCurEntry = pCtrlEntry; + Create(); + sal_uInt16 nY = pCtrlEntry->nY; + sal_uInt16 nX = pCtrlEntry->nX; + DBG_ASSERT(nY<nRows,"GoUpDown:Bad column"); + DBG_ASSERT(nX<nCols,"GoUpDown:Bad row"); + + // neighbor in same column? + if( bDown ) + pResult = SearchCol( + nX, nY, sal::static_int_cast< sal_uInt16 >(nRows-1), true, true ); + else + pResult = SearchCol( nX, 0, nY, false, true ); + if( pResult ) + return pResult; + + tools::Long nCurRow = nY; + + tools::Long nRowOffs, nLastRow; + if( bDown ) + { + nRowOffs = 1; + nLastRow = nRows; + } + else + { + nRowOffs = -1; + nLastRow = -1; // 0-1 + } + + sal_uInt16 nColMin = nX; + sal_uInt16 nColMax = nX; + do + { + SvxIconChoiceCtrlEntry* pEntry = SearchRow(static_cast<sal_uInt16>(nCurRow), nColMin, nColMax, true, false); + if( pEntry ) + return pEntry; + if( nColMin ) + nColMin--; + if( nColMax < (nCols-1)) + nColMax++; + nCurRow += nRowOffs; + } while( nCurRow != nLastRow ); + return nullptr; +} + +void IcnCursor_Impl::SetDeltas() +{ + const Size& rSize = pView->aVirtOutputSize; + nCols = rSize.Width() / pView->nGridDX; + if( !nCols ) + nCols = 1; + nRows = rSize.Height() / pView->nGridDY; + if( (nRows * pView->nGridDY) < rSize.Height() ) + nRows++; + if( !nRows ) + nRows = 1; + + nDeltaWidth = static_cast<short>(rSize.Width() / nCols); + nDeltaHeight = static_cast<short>(rSize.Height() / nRows); + if( !nDeltaHeight ) + { + nDeltaHeight = 1; + SAL_INFO("vcl", "SetDeltas:Bad height"); + } + if( !nDeltaWidth ) + { + nDeltaWidth = 1; + SAL_INFO("vcl", "SetDeltas:Bad width"); + } +} + +IcnGridMap_Impl::IcnGridMap_Impl(SvxIconChoiceCtrl_Impl* pView) + : _pView(pView), _nGridCols(0), _nGridRows(0) +{ +} + +IcnGridMap_Impl::~IcnGridMap_Impl() +{ +} + +void IcnGridMap_Impl::Expand() +{ + if( !_pGridMap ) + Create_Impl(); + else + { + sal_uInt16 nNewGridRows = _nGridRows; + sal_uInt16 nNewGridCols = _nGridCols; + if( _pView->nWinBits & WB_ALIGN_TOP ) + nNewGridRows += 50; + else + nNewGridCols += 50; + + size_t nNewCellCount = static_cast<size_t>(nNewGridRows) * nNewGridCols; + bool* pNewGridMap = new bool[nNewCellCount]; + size_t nOldCellCount = static_cast<size_t>(_nGridRows) * _nGridCols; + memcpy(pNewGridMap, _pGridMap.get(), nOldCellCount * sizeof(bool)); + memset(pNewGridMap + nOldCellCount, 0, (nNewCellCount-nOldCellCount) * sizeof(bool)); + _pGridMap.reset( pNewGridMap ); + _nGridRows = nNewGridRows; + _nGridCols = nNewGridCols; + } +} + +void IcnGridMap_Impl::Create_Impl() +{ + DBG_ASSERT(!_pGridMap,"Unnecessary call to IcnGridMap_Impl::Create_Impl()"); + if( _pGridMap ) + return; + GetMinMapSize( _nGridCols, _nGridRows ); + if( _pView->nWinBits & WB_ALIGN_TOP ) + _nGridRows += 50; // avoid resize of gridmap too often + else + _nGridCols += 50; + + size_t nCellCount = static_cast<size_t>(_nGridRows) * _nGridCols; + _pGridMap.reset( new bool[nCellCount] ); + memset(_pGridMap.get(), 0, nCellCount * sizeof(bool)); + + const size_t nCount = _pView->maEntries.size(); + for( size_t nCur=0; nCur < nCount; nCur++ ) + OccupyGrids( _pView->maEntries[ nCur ].get() ); +} + +void IcnGridMap_Impl::GetMinMapSize( sal_uInt16& rDX, sal_uInt16& rDY ) const +{ + tools::Long nX, nY; + if( _pView->nWinBits & WB_ALIGN_TOP ) + { + // The view grows in vertical direction. Its max. width is _pView->nMaxVirtWidth + nX = _pView->nMaxVirtWidth; + if( !nX ) + nX = _pView->pView->GetOutputSizePixel().Width(); + if( !(_pView->nFlags & IconChoiceFlags::Arranging) ) + nX -= _pView->nVerSBarWidth; + + nY = _pView->aVirtOutputSize.Height(); + } + else + { + // The view grows in horizontal direction. Its max. height is _pView->nMaxVirtHeight + nY = _pView->nMaxVirtHeight; + if( !nY ) + nY = _pView->pView->GetOutputSizePixel().Height(); + if( !(_pView->nFlags & IconChoiceFlags::Arranging) ) + nY -= _pView->nHorSBarHeight; + nX = _pView->aVirtOutputSize.Width(); + } + + if( !nX ) + nX = DEFAULT_MAX_VIRT_WIDTH; + if( !nY ) + nY = DEFAULT_MAX_VIRT_HEIGHT; + + tools::Long nDX = nX / _pView->nGridDX; + tools::Long nDY = nY / _pView->nGridDY; + + if( !nDX ) + nDX++; + if( !nDY ) + nDY++; + + rDX = static_cast<sal_uInt16>(nDX); + rDY = static_cast<sal_uInt16>(nDY); +} + +GridId IcnGridMap_Impl::GetGrid( sal_uInt16 nGridX, sal_uInt16 nGridY ) +{ + Create(); + if( _pView->nWinBits & WB_ALIGN_TOP ) + return nGridX + ( static_cast<GridId>(nGridY) * _nGridCols ); + else + return nGridY + ( static_cast<GridId>(nGridX) * _nGridRows ); +} + +GridId IcnGridMap_Impl::GetGrid( const Point& rDocPos ) +{ + Create(); + + tools::Long nX = rDocPos.X(); + tools::Long nY = rDocPos.Y(); + nX -= LROFFS_WINBORDER; + nY -= TBOFFS_WINBORDER; + nX /= _pView->nGridDX; + nY /= _pView->nGridDY; + if( nX >= _nGridCols ) + { + nX = _nGridCols - 1; + } + if( nY >= _nGridRows ) + { + nY = _nGridRows - 1; + } + GridId nId = GetGrid( static_cast<sal_uInt16>(nX), static_cast<sal_uInt16>(nY) ); + DBG_ASSERT(nId <o3tl::make_unsigned(_nGridCols*_nGridRows),"GetGrid failed"); + return nId; +} + +tools::Rectangle IcnGridMap_Impl::GetGridRect( GridId nId ) +{ + Create(); + sal_uInt16 nGridX, nGridY; + GetGridCoord( nId, nGridX, nGridY ); + const tools::Long nLeft = nGridX * _pView->nGridDX+ LROFFS_WINBORDER; + const tools::Long nTop = nGridY * _pView->nGridDY + TBOFFS_WINBORDER; + return tools::Rectangle( + nLeft, nTop, + nLeft + _pView->nGridDX, + nTop + _pView->nGridDY ); +} + +GridId IcnGridMap_Impl::GetUnoccupiedGrid() +{ + Create(); + sal_uLong nStart = 0; + bool bExpanded = false; + + while( true ) + { + const sal_uLong nCount = static_cast<sal_uInt16>(_nGridCols * _nGridRows); + for( sal_uLong nCur = nStart; nCur < nCount; nCur++ ) + { + if( !_pGridMap[ nCur ] ) + { + _pGridMap[ nCur ] = true; + return static_cast<GridId>(nCur); + } + } + DBG_ASSERT(!bExpanded,"ExpandGrid failed"); + if( bExpanded ) + return 0; // prevent never ending loop + bExpanded = true; + Expand(); + nStart = nCount; + } +} + +// An entry only means that there's a GridRect lying under its center. This +// variant is much faster than allocating via the bounding rectangle but can +// lead to small overlaps. +void IcnGridMap_Impl::OccupyGrids( const SvxIconChoiceCtrlEntry* pEntry ) +{ + if( !_pGridMap || !SvxIconChoiceCtrl_Impl::IsBoundingRectValid( pEntry->aRect )) + return; + OccupyGrid( GetGrid( pEntry->aRect.Center()) ); +} + +void IcnGridMap_Impl::Clear() +{ + if( _pGridMap ) + { + _pGridMap.reset(); + _nGridRows = 0; + _nGridCols = 0; + _aLastOccupiedGrid.SetEmpty(); + } +} + +sal_uLong IcnGridMap_Impl::GetGridCount( const Size& rSizePixel, sal_uInt16 nDX, sal_uInt16 nDY) +{ + tools::Long ndx = (rSizePixel.Width() - LROFFS_WINBORDER) / nDX; + if( ndx < 0 ) ndx *= -1; + tools::Long ndy = (rSizePixel.Height() - TBOFFS_WINBORDER) / nDY; + if( ndy < 0 ) ndy *= -1; + return static_cast<sal_uLong>(ndx * ndy); +} + +void IcnGridMap_Impl::OutputSizeChanged() +{ + if( !_pGridMap ) + return; + + sal_uInt16 nCols, nRows; + GetMinMapSize( nCols, nRows ); + if( _pView->nWinBits & WB_ALIGN_TOP ) + { + if( nCols != _nGridCols ) + Clear(); + else if( nRows >= _nGridRows ) + Expand(); + } + else + { + if( nRows != _nGridRows ) + Clear(); + else if( nCols >= _nGridCols ) + Expand(); + } +} + +// Independently of the view's alignment (TOP or LEFT), the gridmap +// should contain the data in a continuous region, to make it possible +// to copy the whole block if the gridmap needs to be expanded. +void IcnGridMap_Impl::GetGridCoord( GridId nId, sal_uInt16& rGridX, sal_uInt16& rGridY ) +{ + Create(); + if( _pView->nWinBits & WB_ALIGN_TOP ) + { + rGridX = static_cast<sal_uInt16>(nId % _nGridCols); + rGridY = static_cast<sal_uInt16>(nId / _nGridCols); + } + else + { + rGridX = static_cast<sal_uInt16>(nId / _nGridRows); + rGridY = static_cast<sal_uInt16>(nId % _nGridRows); + } +} + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/imp_listbox.cxx b/vcl/source/control/imp_listbox.cxx new file mode 100644 index 000000000..fa01c647e --- /dev/null +++ b/vcl/source/control/imp_listbox.cxx @@ -0,0 +1,3012 @@ +/* -*- 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 <memory> + +#include <vcl/svapp.hxx> +#include <vcl/settings.hxx> +#include <vcl/event.hxx> +#include <vcl/scrbar.hxx> +#include <vcl/toolkit/lstbox.hxx> +#include <vcl/i18nhelp.hxx> +#include <vcl/naturalsort.hxx> + +#include <listbox.hxx> +#include <svdata.hxx> +#include <window.h> + +#include <com/sun/star/accessibility/AccessibleRole.hpp> + +#include <sal/log.hxx> +#include <o3tl/safeint.hxx> +#include <o3tl/string_view.hxx> +#include <osl/diagnose.h> +#include <comphelper/string.hxx> +#include <comphelper/processfactory.hxx> + +#include <limits> + +#define MULTILINE_ENTRY_DRAW_FLAGS ( DrawTextFlags::WordBreak | DrawTextFlags::MultiLine | DrawTextFlags::VCenter ) + +using namespace ::com::sun::star; + +constexpr tools::Long gnBorder = 1; + +void ImplInitDropDownButton( PushButton* pButton ) +{ + pButton->SetSymbol( SymbolType::SPIN_DOWN ); + + if ( pButton->IsNativeControlSupported(ControlType::Listbox, ControlPart::Entire) + && ! pButton->IsNativeControlSupported(ControlType::Listbox, ControlPart::ButtonDown) ) + pButton->SetBackground(); +} + +ImplEntryList::ImplEntryList( vcl::Window* pWindow ) +{ + mpWindow = pWindow; + mnLastSelected = LISTBOX_ENTRY_NOTFOUND; + mnSelectionAnchor = LISTBOX_ENTRY_NOTFOUND; + mnImages = 0; + mbCallSelectionChangedHdl = true; + + mnMRUCount = 0; + mnMaxMRUCount = 0; +} + +ImplEntryList::~ImplEntryList() +{ + Clear(); +} + +void ImplEntryList::Clear() +{ + mnImages = 0; + maEntries.clear(); +} + +void ImplEntryList::dispose() +{ + Clear(); + mpWindow.clear(); +} + +void ImplEntryList::SelectEntry( sal_Int32 nPos, bool bSelect ) +{ + if (0 <= nPos && o3tl::make_unsigned(nPos) < maEntries.size()) + { + std::vector<std::unique_ptr<ImplEntryType> >::iterator iter = maEntries.begin()+nPos; + + if ( ( (*iter)->mbIsSelected != bSelect ) && + ( ( (*iter)->mnFlags & ListBoxEntryFlags::DisableSelection) == ListBoxEntryFlags::NONE ) ) + { + (*iter)->mbIsSelected = bSelect; + if ( mbCallSelectionChangedHdl ) + maSelectionChangedHdl.Call( nPos ); + } + } +} + +namespace +{ + comphelper::string::NaturalStringSorter& GetSorter() + { + static comphelper::string::NaturalStringSorter gSorter( + ::comphelper::getProcessComponentContext(), + Application::GetSettings().GetLanguageTag().getLocale()); + return gSorter; + }; +} + +namespace vcl +{ + sal_Int32 NaturalSortCompare(const OUString &rA, const OUString &rB) + { + const comphelper::string::NaturalStringSorter &rSorter = GetSorter(); + return rSorter.compare(rA, rB); + } +} + +sal_Int32 ImplEntryList::InsertEntry( sal_Int32 nPos, ImplEntryType* pNewEntry, bool bSort ) +{ + assert(nPos >= 0); + assert(maEntries.size() < LISTBOX_MAX_ENTRIES); + + if ( !!pNewEntry->maImage ) + mnImages++; + + sal_Int32 insPos = 0; + const sal_Int32 nEntriesSize = static_cast<sal_Int32>(maEntries.size()); + + if ( !bSort || maEntries.empty()) + { + if (0 <= nPos && nPos < nEntriesSize) + { + insPos = nPos; + maEntries.insert( maEntries.begin() + nPos, std::unique_ptr<ImplEntryType>(pNewEntry) ); + } + else + { + insPos = nEntriesSize; + maEntries.push_back(std::unique_ptr<ImplEntryType>(pNewEntry)); + } + } + else + { + const comphelper::string::NaturalStringSorter &rSorter = GetSorter(); + + const OUString& rStr = pNewEntry->maStr; + + ImplEntryType* pTemp = GetEntry( nEntriesSize-1 ); + + try + { + sal_Int32 nComp = rSorter.compare(rStr, pTemp->maStr); + + // fast insert for sorted data + if ( nComp >= 0 ) + { + insPos = nEntriesSize; + maEntries.push_back(std::unique_ptr<ImplEntryType>(pNewEntry)); + } + else + { + pTemp = GetEntry( mnMRUCount ); + + nComp = rSorter.compare(rStr, pTemp->maStr); + if ( nComp <= 0 ) + { + insPos = 0; + maEntries.insert(maEntries.begin(), std::unique_ptr<ImplEntryType>(pNewEntry)); + } + else + { + sal_uLong nLow = mnMRUCount; + sal_uLong nHigh = maEntries.size()-1; + sal_Int32 nMid; + + // binary search + do + { + nMid = static_cast<sal_Int32>((nLow + nHigh) / 2); + pTemp = GetEntry( nMid ); + + nComp = rSorter.compare(rStr, pTemp->maStr); + + if ( nComp < 0 ) + nHigh = nMid-1; + else + { + if ( nComp > 0 ) + nLow = nMid + 1; + else + break; + } + } + while ( nLow <= nHigh ); + + if ( nComp >= 0 ) + nMid++; + + insPos = nMid; + maEntries.insert(maEntries.begin()+nMid, std::unique_ptr<ImplEntryType>(pNewEntry)); + } + } + } + catch (uno::RuntimeException& ) + { + // XXX this is arguable, if the exception occurred because pNewEntry is + // garbage you wouldn't insert it. If the exception occurred because the + // Collator implementation is garbage then give the user a chance to see + // his stuff + insPos = 0; + maEntries.insert(maEntries.begin(), std::unique_ptr<ImplEntryType>(pNewEntry)); + } + + } + + return insPos; +} + +void ImplEntryList::RemoveEntry( sal_Int32 nPos ) +{ + if (0 <= nPos && o3tl::make_unsigned(nPos) < maEntries.size()) + { + std::vector<std::unique_ptr<ImplEntryType> >::iterator iter = maEntries.begin()+ nPos; + + if ( !!(*iter)->maImage ) + mnImages--; + + maEntries.erase(iter); + } +} + +sal_Int32 ImplEntryList::FindEntry( std::u16string_view rString, bool bSearchMRUArea ) const +{ + const sal_Int32 nEntries = static_cast<sal_Int32>(maEntries.size()); + for ( sal_Int32 n = bSearchMRUArea ? 0 : GetMRUCount(); n < nEntries; n++ ) + { + OUString aComp( vcl::I18nHelper::filterFormattingChars( maEntries[n]->maStr ) ); + if ( aComp == rString ) + return n; + } + return LISTBOX_ENTRY_NOTFOUND; +} + +sal_Int32 ImplEntryList::FindMatchingEntry( const OUString& rStr, sal_Int32 nStart, bool bLazy ) const +{ + sal_Int32 nPos = LISTBOX_ENTRY_NOTFOUND; + sal_Int32 nEntryCount = GetEntryCount(); + + const vcl::I18nHelper& rI18nHelper = mpWindow->GetSettings().GetLocaleI18nHelper(); + for ( sal_Int32 n = nStart; n < nEntryCount; ) + { + ImplEntryType* pImplEntry = GetEntry( n ); + bool bMatch; + if ( bLazy ) + { + bMatch = rI18nHelper.MatchString( rStr, pImplEntry->maStr ); + } + else + { + bMatch = pImplEntry->maStr.startsWith(rStr); + } + if ( bMatch ) + { + nPos = n; + break; + } + + n++; + } + + return nPos; +} + +tools::Long ImplEntryList::GetAddedHeight( sal_Int32 i_nEndIndex, sal_Int32 i_nBeginIndex ) const +{ + tools::Long nHeight = 0; + sal_Int32 nStart = std::min(i_nEndIndex, i_nBeginIndex); + sal_Int32 nStop = std::max(i_nEndIndex, i_nBeginIndex); + sal_Int32 nEntryCount = GetEntryCount(); + if( 0 <= nStop && nStop != LISTBOX_ENTRY_NOTFOUND && nEntryCount != 0 ) + { + // sanity check + if( nStop > nEntryCount-1 ) + nStop = nEntryCount-1; + if (nStart < 0) + nStart = 0; + else if( nStart > nEntryCount-1 ) + nStart = nEntryCount-1; + + sal_Int32 nIndex = nStart; + while( nIndex != LISTBOX_ENTRY_NOTFOUND && nIndex < nStop ) + { + tools::Long nPosHeight = GetEntryPtr( nIndex )->getHeightWithMargin(); + if (nHeight > ::std::numeric_limits<tools::Long>::max() - nPosHeight) + { + SAL_WARN( "vcl", "ImplEntryList::GetAddedHeight: truncated"); + break; + } + nHeight += nPosHeight; + nIndex++; + } + } + else + nHeight = 0; + return i_nEndIndex > i_nBeginIndex ? nHeight : -nHeight; +} + +tools::Long ImplEntryList::GetEntryHeight( sal_Int32 nPos ) const +{ + ImplEntryType* pImplEntry = GetEntry( nPos ); + return pImplEntry ? pImplEntry->getHeightWithMargin() : 0; +} + +OUString ImplEntryList::GetEntryText( sal_Int32 nPos ) const +{ + OUString aEntryText; + ImplEntryType* pImplEntry = GetEntry( nPos ); + if ( pImplEntry ) + aEntryText = pImplEntry->maStr; + return aEntryText; +} + +bool ImplEntryList::HasEntryImage( sal_Int32 nPos ) const +{ + bool bImage = false; + ImplEntryType* pImplEntry = GetEntry( nPos ); + if ( pImplEntry ) + bImage = !!pImplEntry->maImage; + return bImage; +} + +Image ImplEntryList::GetEntryImage( sal_Int32 nPos ) const +{ + Image aImage; + ImplEntryType* pImplEntry = GetEntry( nPos ); + if ( pImplEntry ) + aImage = pImplEntry->maImage; + return aImage; +} + +void ImplEntryList::SetEntryData( sal_Int32 nPos, void* pNewData ) +{ + ImplEntryType* pImplEntry = GetEntry( nPos ); + if ( pImplEntry ) + pImplEntry->mpUserData = pNewData; +} + +void* ImplEntryList::GetEntryData( sal_Int32 nPos ) const +{ + ImplEntryType* pImplEntry = GetEntry( nPos ); + return pImplEntry ? pImplEntry->mpUserData : nullptr; +} + +void ImplEntryList::SetEntryFlags( sal_Int32 nPos, ListBoxEntryFlags nFlags ) +{ + ImplEntryType* pImplEntry = GetEntry( nPos ); + if ( pImplEntry ) + pImplEntry->mnFlags = nFlags; +} + +sal_Int32 ImplEntryList::GetSelectedEntryCount() const +{ + sal_Int32 nSelCount = 0; + for ( sal_Int32 n = GetEntryCount(); n; ) + { + ImplEntryType* pImplEntry = GetEntry( --n ); + if ( pImplEntry->mbIsSelected ) + nSelCount++; + } + return nSelCount; +} + +OUString ImplEntryList::GetSelectedEntry( sal_Int32 nIndex ) const +{ + return GetEntryText( GetSelectedEntryPos( nIndex ) ); +} + +sal_Int32 ImplEntryList::GetSelectedEntryPos( sal_Int32 nIndex ) const +{ + sal_Int32 nSelEntryPos = LISTBOX_ENTRY_NOTFOUND; + sal_Int32 nSel = 0; + sal_Int32 nEntryCount = GetEntryCount(); + + for ( sal_Int32 n = 0; n < nEntryCount; n++ ) + { + ImplEntryType* pImplEntry = GetEntry( n ); + if ( pImplEntry->mbIsSelected ) + { + if ( nSel == nIndex ) + { + nSelEntryPos = n; + break; + } + nSel++; + } + } + + return nSelEntryPos; +} + +bool ImplEntryList::IsEntryPosSelected( sal_Int32 nIndex ) const +{ + ImplEntryType* pImplEntry = GetEntry( nIndex ); + return pImplEntry && pImplEntry->mbIsSelected; +} + +bool ImplEntryList::IsEntrySelectable( sal_Int32 nPos ) const +{ + ImplEntryType* pImplEntry = GetEntry( nPos ); + return pImplEntry == nullptr || ((pImplEntry->mnFlags & ListBoxEntryFlags::DisableSelection) == ListBoxEntryFlags::NONE); +} + +sal_Int32 ImplEntryList::FindFirstSelectable( sal_Int32 nPos, bool bForward /* = true */ ) const +{ + if( IsEntrySelectable( nPos ) ) + return nPos; + + if( bForward ) + { + for( nPos = nPos + 1; nPos < GetEntryCount(); nPos++ ) + { + if( IsEntrySelectable( nPos ) ) + return nPos; + } + } + else + { + while( nPos ) + { + nPos--; + if( IsEntrySelectable( nPos ) ) + return nPos; + } + } + + return LISTBOX_ENTRY_NOTFOUND; +} + +ImplListBoxWindow::ImplListBoxWindow( vcl::Window* pParent, WinBits nWinStyle ) : + Control( pParent, 0 ), + maEntryList( this ), + maQuickSelectionEngine( *this ) +{ + + mnTop = 0; + mnLeft = 0; + mnSelectModifier = 0; + mnUserDrawEntry = LISTBOX_ENTRY_NOTFOUND; + mbTrack = false; + mbTravelSelect = false; + mbTrackingSelect = false; + mbSelectionChanged = false; + mbMouseMoveSelect = false; + mbMulti = false; + mbGrabFocus = false; + mbUserDrawEnabled = false; + mbInUserDraw = false; + mbReadOnly = false; + mbHasFocusRect = false; + mbRight = ( nWinStyle & WB_RIGHT ); + mbCenter = ( nWinStyle & WB_CENTER ); + mbSimpleMode = ( nWinStyle & WB_SIMPLEMODE ); + mbSort = ( nWinStyle & WB_SORT ); + mbIsDropdown = ( nWinStyle & WB_DROPDOWN ); + mbEdgeBlending = false; + + mnCurrentPos = LISTBOX_ENTRY_NOTFOUND; + mnTrackingSaveSelection = LISTBOX_ENTRY_NOTFOUND; + + GetOutDev()->SetLineColor(); + SetTextFillColor(); + SetBackground( Wallpaper( GetSettings().GetStyleSettings().GetFieldColor() ) ); + + ApplySettings(*GetOutDev()); + ImplCalcMetrics(); +} + +ImplListBoxWindow::~ImplListBoxWindow() +{ + disposeOnce(); +} + +void ImplListBoxWindow::dispose() +{ + maEntryList.dispose(); + Control::dispose(); +} + +void ImplListBoxWindow::ApplySettings(vcl::RenderContext& rRenderContext) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + ApplyControlFont(rRenderContext, rStyleSettings.GetFieldFont()); + ApplyControlForeground(rRenderContext, rStyleSettings.GetFieldTextColor()); + + if (IsControlBackground()) + rRenderContext.SetBackground(GetControlBackground()); + else + rRenderContext.SetBackground(rStyleSettings.GetFieldColor()); +} + +void ImplListBoxWindow::ImplCalcMetrics() +{ + mnMaxWidth = 0; + mnMaxTxtWidth = 0; + mnMaxImgWidth = 0; + mnMaxImgTxtWidth= 0; + mnMaxImgHeight = 0; + + mnTextHeight = static_cast<sal_uInt16>(GetTextHeight()); + mnMaxTxtHeight = mnTextHeight + gnBorder; + mnMaxHeight = mnMaxTxtHeight; + + if ( maUserItemSize.Height() > mnMaxHeight ) + mnMaxHeight = static_cast<sal_uInt16>(maUserItemSize.Height()); + if ( maUserItemSize.Width() > mnMaxWidth ) + mnMaxWidth= static_cast<sal_uInt16>(maUserItemSize.Width()); + + for ( sal_Int32 n = maEntryList.GetEntryCount(); n; ) + { + ImplEntryType* pEntry = maEntryList.GetMutableEntryPtr( --n ); + ImplUpdateEntryMetrics( *pEntry ); + } + + if( mnCurrentPos != LISTBOX_ENTRY_NOTFOUND ) + { + Size aSz( GetOutputSizePixel().Width(), maEntryList.GetEntryPtr( mnCurrentPos )->getHeightWithMargin() ); + maFocusRect.SetSize( aSz ); + } +} + +void ImplListBoxWindow::Clear() +{ + maEntryList.Clear(); + + mnMaxHeight = mnMaxTxtHeight; + mnMaxWidth = 0; + mnMaxTxtWidth = 0; + mnMaxImgTxtWidth= 0; + mnMaxImgWidth = 0; + mnMaxImgHeight = 0; + mnTop = 0; + mnLeft = 0; + ImplClearLayoutData(); + + mnCurrentPos = LISTBOX_ENTRY_NOTFOUND; + maQuickSelectionEngine.Reset(); + + Invalidate(); +} + +void ImplListBoxWindow::SetUserItemSize( const Size& rSz ) +{ + ImplClearLayoutData(); + maUserItemSize = rSz; + ImplCalcMetrics(); +} + +namespace { + +struct ImplEntryMetrics +{ + bool bText; + bool bImage; + tools::Long nEntryWidth; + tools::Long nEntryHeight; + tools::Long nTextWidth; + tools::Long nImgWidth; + tools::Long nImgHeight; +}; + +} + +tools::Long ImplEntryType::getHeightWithMargin() const +{ + return mnHeight + ImplGetSVData()->maNWFData.mnListBoxEntryMargin; +} + +SalLayoutGlyphs* ImplEntryType::GetTextGlyphs(const OutputDevice* pOutputDevice) +{ + if (maStrGlyphs.IsValid()) + // Use pre-calculated result. + return &maStrGlyphs; + + std::unique_ptr<SalLayout> pLayout = pOutputDevice->ImplLayout( + maStr, 0, maStr.getLength(), Point(0, 0), 0, {}, SalLayoutFlags::GlyphItemsOnly); + if (!pLayout) + return nullptr; + + // Remember the calculation result. + maStrGlyphs = pLayout->GetGlyphs(); + + return &maStrGlyphs; +} + +void ImplListBoxWindow::ImplUpdateEntryMetrics( ImplEntryType& rEntry ) +{ + ImplEntryMetrics aMetrics; + aMetrics.bText = !rEntry.maStr.isEmpty(); + aMetrics.bImage = !!rEntry.maImage; + aMetrics.nEntryWidth = 0; + aMetrics.nEntryHeight = 0; + aMetrics.nTextWidth = 0; + aMetrics.nImgWidth = 0; + aMetrics.nImgHeight = 0; + + if ( aMetrics.bText ) + { + if( rEntry.mnFlags & ListBoxEntryFlags::MultiLine ) + { + // multiline case + Size aCurSize( PixelToLogic( GetSizePixel() ) ); + // set the current size to a large number + // GetTextRect should shrink it to the actual size + aCurSize.setHeight( 0x7fffff ); + tools::Rectangle aTextRect( Point( 0, 0 ), aCurSize ); + aTextRect = GetTextRect( aTextRect, rEntry.maStr, DrawTextFlags::WordBreak | DrawTextFlags::MultiLine ); + aMetrics.nTextWidth = aTextRect.GetWidth(); + if( aMetrics.nTextWidth > mnMaxTxtWidth ) + mnMaxTxtWidth = aMetrics.nTextWidth; + aMetrics.nEntryWidth = mnMaxTxtWidth; + aMetrics.nEntryHeight = aTextRect.GetHeight() + gnBorder; + } + else + { + // normal single line case + const SalLayoutGlyphs* pGlyphs = rEntry.GetTextGlyphs(GetOutDev()); + aMetrics.nTextWidth + = static_cast<sal_uInt16>(GetTextWidth(rEntry.maStr, 0, -1, nullptr, pGlyphs)); + if( aMetrics.nTextWidth > mnMaxTxtWidth ) + mnMaxTxtWidth = aMetrics.nTextWidth; + aMetrics.nEntryWidth = mnMaxTxtWidth; + aMetrics.nEntryHeight = mnTextHeight + gnBorder; + } + } + if ( aMetrics.bImage ) + { + Size aImgSz = rEntry.maImage.GetSizePixel(); + aMetrics.nImgWidth = static_cast<sal_uInt16>(CalcZoom( aImgSz.Width() )); + aMetrics.nImgHeight = static_cast<sal_uInt16>(CalcZoom( aImgSz.Height() )); + + if( aMetrics.nImgWidth > mnMaxImgWidth ) + mnMaxImgWidth = aMetrics.nImgWidth; + if( aMetrics.nImgHeight > mnMaxImgHeight ) + mnMaxImgHeight = aMetrics.nImgHeight; + + mnMaxImgTxtWidth = std::max( mnMaxImgTxtWidth, aMetrics.nTextWidth ); + aMetrics.nEntryHeight = std::max( aMetrics.nImgHeight, aMetrics.nEntryHeight ); + + } + + bool bIsUserDrawEnabled = IsUserDrawEnabled(); + if (bIsUserDrawEnabled || aMetrics.bImage) + { + aMetrics.nEntryWidth = std::max( aMetrics.nImgWidth, maUserItemSize.Width() ); + if (!bIsUserDrawEnabled && aMetrics.bText) + aMetrics.nEntryWidth += aMetrics.nTextWidth + IMG_TXT_DISTANCE; + aMetrics.nEntryHeight = std::max( std::max( mnMaxImgHeight, maUserItemSize.Height() ) + 2, + aMetrics.nEntryHeight ); + } + + if (!aMetrics.bText && !aMetrics.bImage && !bIsUserDrawEnabled) + { + // entries which have no (aka an empty) text, and no image, + // and are not user-drawn, should be shown nonetheless + aMetrics.nEntryHeight = mnTextHeight + gnBorder; + } + + if ( aMetrics.nEntryWidth > mnMaxWidth ) + mnMaxWidth = aMetrics.nEntryWidth; + if ( aMetrics.nEntryHeight > mnMaxHeight ) + mnMaxHeight = aMetrics.nEntryHeight; + + rEntry.mnHeight = aMetrics.nEntryHeight; +} + +void ImplListBoxWindow::ImplCallSelect() +{ + if ( !IsTravelSelect() && GetEntryList().GetMaxMRUCount() ) + { + // Insert the selected entry as MRU, if not already first MRU + sal_Int32 nSelected = GetEntryList().GetSelectedEntryPos( 0 ); + sal_Int32 nMRUCount = GetEntryList().GetMRUCount(); + OUString aSelected = GetEntryList().GetEntryText( nSelected ); + sal_Int32 nFirstMatchingEntryPos = GetEntryList().FindEntry( aSelected, true ); + if ( nFirstMatchingEntryPos || !nMRUCount ) + { + bool bSelectNewEntry = false; + if ( nFirstMatchingEntryPos < nMRUCount ) + { + RemoveEntry( nFirstMatchingEntryPos ); + nMRUCount--; + if ( nFirstMatchingEntryPos == nSelected ) + bSelectNewEntry = true; + } + else if ( nMRUCount == GetEntryList().GetMaxMRUCount() ) + { + RemoveEntry( nMRUCount - 1 ); + nMRUCount--; + } + + ImplClearLayoutData(); + + ImplEntryType* pNewEntry = new ImplEntryType( aSelected ); + pNewEntry->mbIsSelected = bSelectNewEntry; + GetEntryList().InsertEntry( 0, pNewEntry, false ); + ImplUpdateEntryMetrics( *pNewEntry ); + GetEntryList().SetMRUCount( ++nMRUCount ); + SetSeparatorPos( nMRUCount ? nMRUCount-1 : 0 ); + maMRUChangedHdl.Call( nullptr ); + } + } + + maSelectHdl.Call( nullptr ); + mbSelectionChanged = false; +} + +sal_Int32 ImplListBoxWindow::InsertEntry(sal_Int32 nPos, ImplEntryType* pNewEntry, bool bSort) +{ + assert(nPos >= 0); + assert(maEntryList.GetEntryCount() < LISTBOX_MAX_ENTRIES); + + ImplClearLayoutData(); + sal_Int32 nNewPos = maEntryList.InsertEntry( nPos, pNewEntry, bSort ); + + if( GetStyle() & WB_WORDBREAK ) + pNewEntry->mnFlags |= ListBoxEntryFlags::MultiLine; + + ImplUpdateEntryMetrics( *pNewEntry ); + return nNewPos; +} + +sal_Int32 ImplListBoxWindow::InsertEntry( sal_Int32 nPos, ImplEntryType* pNewEntry ) +{ + return InsertEntry(nPos, pNewEntry, mbSort); +} + +void ImplListBoxWindow::RemoveEntry( sal_Int32 nPos ) +{ + ImplClearLayoutData(); + maEntryList.RemoveEntry( nPos ); + if( mnCurrentPos >= maEntryList.GetEntryCount() ) + mnCurrentPos = LISTBOX_ENTRY_NOTFOUND; + ImplCalcMetrics(); +} + +void ImplListBoxWindow::SetEntryFlags( sal_Int32 nPos, ListBoxEntryFlags nFlags ) +{ + maEntryList.SetEntryFlags( nPos, nFlags ); + ImplEntryType* pEntry = maEntryList.GetMutableEntryPtr( nPos ); + if( pEntry ) + ImplUpdateEntryMetrics( *pEntry ); +} + +void ImplListBoxWindow::ImplShowFocusRect() +{ + if ( mbHasFocusRect ) + HideFocus(); + ShowFocus( maFocusRect ); + mbHasFocusRect = true; +} + +void ImplListBoxWindow::ImplHideFocusRect() +{ + if ( mbHasFocusRect ) + { + HideFocus(); + mbHasFocusRect = false; + } +} + +sal_Int32 ImplListBoxWindow::GetEntryPosForPoint( const Point& rPoint ) const +{ + tools::Long nY = gnBorder; + + sal_Int32 nSelect = mnTop; + const ImplEntryType* pEntry = maEntryList.GetEntryPtr( nSelect ); + while (pEntry) + { + tools::Long nEntryHeight = pEntry->getHeightWithMargin(); + if (rPoint.Y() <= nEntryHeight + nY) + break; + nY += nEntryHeight; + pEntry = maEntryList.GetEntryPtr( ++nSelect ); + } + if( pEntry == nullptr ) + nSelect = LISTBOX_ENTRY_NOTFOUND; + + return nSelect; +} + +bool ImplListBoxWindow::IsVisible( sal_Int32 i_nEntry ) const +{ + bool bRet = false; + + if( i_nEntry >= mnTop ) + { + if( maEntryList.GetAddedHeight( i_nEntry, mnTop ) < + PixelToLogic( GetSizePixel() ).Height() ) + { + bRet = true; + } + } + + return bRet; +} + +tools::Long ImplListBoxWindow::GetEntryHeightWithMargin() const +{ + tools::Long nMargin = ImplGetSVData()->maNWFData.mnListBoxEntryMargin; + return mnMaxHeight + nMargin; +} + +sal_Int32 ImplListBoxWindow::GetLastVisibleEntry() const +{ + sal_Int32 nPos = mnTop; + tools::Long nWindowHeight = GetSizePixel().Height(); + sal_Int32 nCount = maEntryList.GetEntryCount(); + tools::Long nDiff; + for( nDiff = 0; nDiff < nWindowHeight && nPos < nCount; nDiff = maEntryList.GetAddedHeight( nPos, mnTop ) ) + nPos++; + + if( nDiff > nWindowHeight && nPos > mnTop ) + nPos--; + + if( nPos >= nCount ) + nPos = nCount-1; + + return nPos; +} + +void ImplListBoxWindow::MouseButtonDown( const MouseEvent& rMEvt ) +{ + mbMouseMoveSelect = false; // only till the first MouseButtonDown + maQuickSelectionEngine.Reset(); + + if ( !IsReadOnly() ) + { + if( rMEvt.GetClicks() == 1 ) + { + sal_Int32 nSelect = GetEntryPosForPoint( rMEvt.GetPosPixel() ); + if( nSelect != LISTBOX_ENTRY_NOTFOUND ) + { + if ( !mbMulti && GetEntryList().GetSelectedEntryCount() ) + mnTrackingSaveSelection = GetEntryList().GetSelectedEntryPos( 0 ); + else + mnTrackingSaveSelection = LISTBOX_ENTRY_NOTFOUND; + + mnCurrentPos = nSelect; + mbTrackingSelect = true; + bool bCurPosChange = (mnCurrentPos != nSelect); + (void)SelectEntries( nSelect, LET_MBDOWN, rMEvt.IsShift(), rMEvt.IsMod1() ,bCurPosChange); + mbTrackingSelect = false; + if ( mbGrabFocus ) + GrabFocus(); + + StartTracking( StartTrackingFlags::ScrollRepeat ); + } + } + if( rMEvt.GetClicks() == 2 ) + { + maDoubleClickHdl.Call( this ); + } + } + else // if ( mbGrabFocus ) + { + GrabFocus(); + } +} + +void ImplListBoxWindow::MouseMove( const MouseEvent& rMEvt ) +{ + if (rMEvt.IsLeaveWindow() || mbMulti || !IsMouseMoveSelect() || !maEntryList.GetEntryCount()) + return; + + tools::Rectangle aRect( Point(), GetOutputSizePixel() ); + if( !aRect.Contains( rMEvt.GetPosPixel() ) ) + return; + + if ( IsMouseMoveSelect() ) + { + sal_Int32 nSelect = GetEntryPosForPoint( rMEvt.GetPosPixel() ); + if( nSelect == LISTBOX_ENTRY_NOTFOUND ) + nSelect = maEntryList.GetEntryCount() - 1; + nSelect = std::min( nSelect, GetLastVisibleEntry() ); + nSelect = std::min( nSelect, static_cast<sal_Int32>( maEntryList.GetEntryCount() - 1 ) ); + // Select only visible Entries with MouseMove, otherwise Tracking... + if ( IsVisible( nSelect ) && + maEntryList.IsEntrySelectable( nSelect ) && + ( ( nSelect != mnCurrentPos ) || !GetEntryList().GetSelectedEntryCount() || ( nSelect != GetEntryList().GetSelectedEntryPos( 0 ) ) ) ) + { + mbTrackingSelect = true; + if ( SelectEntries( nSelect, LET_TRACKING ) ) + { + // When list box selection change by mouse move, notify + // VclEventId::ListboxSelect vcl event. + maListItemSelectHdl.Call(nullptr); + } + mbTrackingSelect = false; + } + } + + // if the DD button was pressed and someone moved into the ListBox + // with the mouse button pressed... + if ( rMEvt.IsLeft() && !rMEvt.IsSynthetic() ) + { + if ( !mbMulti && GetEntryList().GetSelectedEntryCount() ) + mnTrackingSaveSelection = GetEntryList().GetSelectedEntryPos( 0 ); + else + mnTrackingSaveSelection = LISTBOX_ENTRY_NOTFOUND; + + StartTracking( StartTrackingFlags::ScrollRepeat ); + } +} + +void ImplListBoxWindow::DeselectAll() +{ + while ( GetEntryList().GetSelectedEntryCount() ) + { + sal_Int32 nS = GetEntryList().GetSelectedEntryPos( 0 ); + SelectEntry( nS, false ); + } +} + +void ImplListBoxWindow::SelectEntry( sal_Int32 nPos, bool bSelect ) +{ + if( (maEntryList.IsEntryPosSelected( nPos ) == bSelect) || !maEntryList.IsEntrySelectable( nPos ) ) + return; + + ImplHideFocusRect(); + if( bSelect ) + { + if( !mbMulti ) + { + // deselect the selected entry + sal_Int32 nDeselect = GetEntryList().GetSelectedEntryPos( 0 ); + if( nDeselect != LISTBOX_ENTRY_NOTFOUND ) + { + //SelectEntryPos( nDeselect, false ); + GetEntryList().SelectEntry( nDeselect, false ); + if (IsUpdateMode() && IsReallyVisible()) + Invalidate(); + } + } + maEntryList.SelectEntry( nPos, true ); + mnCurrentPos = nPos; + if ( ( nPos != LISTBOX_ENTRY_NOTFOUND ) && IsUpdateMode() ) + { + Invalidate(); + if ( !IsVisible( nPos ) ) + { + ImplClearLayoutData(); + sal_Int32 nVisibleEntries = GetLastVisibleEntry()-mnTop; + if ( !nVisibleEntries || !IsReallyVisible() || ( nPos < GetTopEntry() ) ) + { + Resize(); + ShowProminentEntry( nPos ); + } + else + { + ShowProminentEntry( nPos ); + } + } + } + } + else + { + maEntryList.SelectEntry( nPos, false ); + Invalidate(); + } + mbSelectionChanged = true; +} + +bool ImplListBoxWindow::SelectEntries( sal_Int32 nSelect, LB_EVENT_TYPE eLET, bool bShift, bool bCtrl, bool bSelectPosChange /*=FALSE*/ ) +{ + bool bSelectionChanged = false; + + if( IsEnabled() && maEntryList.IsEntrySelectable( nSelect ) ) + { + bool bFocusChanged = false; + + // here (Single-ListBox) only one entry can be deselected + if( !mbMulti ) + { + sal_Int32 nDeselect = maEntryList.GetSelectedEntryPos( 0 ); + if( nSelect != nDeselect ) + { + SelectEntry( nSelect, true ); + maEntryList.SetLastSelected( nSelect ); + bFocusChanged = true; + bSelectionChanged = true; + } + } + // MultiListBox without Modifier + else if( mbSimpleMode && !bCtrl && !bShift ) + { + sal_Int32 nEntryCount = maEntryList.GetEntryCount(); + for ( sal_Int32 nPos = 0; nPos < nEntryCount; nPos++ ) + { + bool bSelect = nPos == nSelect; + if ( maEntryList.IsEntryPosSelected( nPos ) != bSelect ) + { + SelectEntry( nPos, bSelect ); + bFocusChanged = true; + bSelectionChanged = true; + } + } + maEntryList.SetLastSelected( nSelect ); + maEntryList.SetSelectionAnchor( nSelect ); + } + // MultiListBox only with CTRL/SHIFT or not in SimpleMode + else if( ( !mbSimpleMode /* && !bShift */ ) || ( mbSimpleMode && ( bCtrl || bShift ) ) ) + { + // Space for selection change + if( !bShift && ( ( eLET == LET_KEYSPACE ) || ( eLET == LET_MBDOWN ) ) ) + { + bool bSelect = !maEntryList.IsEntryPosSelected( nSelect ); + SelectEntry( nSelect, bSelect ); + maEntryList.SetLastSelected( nSelect ); + maEntryList.SetSelectionAnchor( nSelect ); + if ( !maEntryList.IsEntryPosSelected( nSelect ) ) + maEntryList.SetSelectionAnchor( LISTBOX_ENTRY_NOTFOUND ); + bFocusChanged = true; + bSelectionChanged = true; + } + else if( ( ( eLET == LET_TRACKING ) && ( nSelect != mnCurrentPos ) ) || + ( bShift && ( ( eLET == LET_KEYMOVE ) || ( eLET == LET_MBDOWN ) ) ) ) + { + mnCurrentPos = nSelect; + bFocusChanged = true; + + sal_Int32 nAnchor = maEntryList.GetSelectionAnchor(); + if( ( nAnchor == LISTBOX_ENTRY_NOTFOUND ) && maEntryList.GetSelectedEntryCount() ) + { + nAnchor = maEntryList.GetSelectedEntryPos( maEntryList.GetSelectedEntryCount() - 1 ); + } + if( nAnchor != LISTBOX_ENTRY_NOTFOUND ) + { + // All entries from Anchor to nSelect have to be selected + sal_Int32 nStart = std::min( nSelect, nAnchor ); + sal_Int32 nEnd = std::max( nSelect, nAnchor ); + for ( sal_Int32 n = nStart; n <= nEnd; n++ ) + { + if ( !maEntryList.IsEntryPosSelected( n ) ) + { + SelectEntry( n, true ); + bSelectionChanged = true; + } + } + + // if appropriate some more has to be deselected... + sal_Int32 nLast = maEntryList.GetLastSelected(); + if ( nLast != LISTBOX_ENTRY_NOTFOUND ) + { + if ( ( nLast > nSelect ) && ( nLast > nAnchor ) ) + { + for ( sal_Int32 n = nSelect+1; n <= nLast; n++ ) + { + if ( maEntryList.IsEntryPosSelected( n ) ) + { + SelectEntry( n, false ); + bSelectionChanged = true; + } + } + } + else if ( ( nLast < nSelect ) && ( nLast < nAnchor ) ) + { + for ( sal_Int32 n = nLast; n < nSelect; n++ ) + { + if ( maEntryList.IsEntryPosSelected( n ) ) + { + SelectEntry( n, false ); + bSelectionChanged = true; + } + } + } + } + maEntryList.SetLastSelected( nSelect ); + } + } + else if( eLET != LET_TRACKING ) + { + ImplHideFocusRect(); + Invalidate(); + bFocusChanged = true; + } + } + else if( bShift ) + { + bFocusChanged = true; + } + + if( bSelectionChanged ) + mbSelectionChanged = true; + + if( bFocusChanged ) + { + tools::Long nHeightDiff = maEntryList.GetAddedHeight( nSelect, mnTop ); + maFocusRect.SetPos( Point( 0, nHeightDiff ) ); + Size aSz( maFocusRect.GetWidth(), + maEntryList.GetEntryHeight( nSelect ) ); + maFocusRect.SetSize( aSz ); + if( HasFocus() ) + ImplShowFocusRect(); + if (bSelectPosChange) + { + maFocusHdl.Call(nSelect); + } + } + ImplClearLayoutData(); + } + return bSelectionChanged; +} + +void ImplListBoxWindow::Tracking( const TrackingEvent& rTEvt ) +{ + tools::Rectangle aRect( Point(), GetOutputSizePixel() ); + bool bInside = aRect.Contains( rTEvt.GetMouseEvent().GetPosPixel() ); + + if( rTEvt.IsTrackingCanceled() || rTEvt.IsTrackingEnded() ) // MouseButtonUp + { + if ( bInside && !rTEvt.IsTrackingCanceled() ) + { + mnSelectModifier = rTEvt.GetMouseEvent().GetModifier(); + ImplCallSelect(); + } + else + { + maCancelHdl.Call( nullptr ); + if ( !mbMulti ) + { + mbTrackingSelect = true; + SelectEntry( mnTrackingSaveSelection, true ); + mbTrackingSelect = false; + if ( mnTrackingSaveSelection != LISTBOX_ENTRY_NOTFOUND ) + { + tools::Long nHeightDiff = maEntryList.GetAddedHeight( mnCurrentPos, mnTop ); + maFocusRect.SetPos( Point( 0, nHeightDiff ) ); + Size aSz( maFocusRect.GetWidth(), + maEntryList.GetEntryHeight( mnCurrentPos ) ); + maFocusRect.SetSize( aSz ); + ImplShowFocusRect(); + } + } + } + + mbTrack = false; + } + else + { + bool bTrackOrQuickClick = mbTrack; + if( !mbTrack ) + { + if ( bInside ) + { + mbTrack = true; + } + + // this case only happens, if the mouse button is pressed very briefly + if( rTEvt.IsTrackingEnded() && mbTrack ) + { + bTrackOrQuickClick = true; + mbTrack = false; + } + } + + if( bTrackOrQuickClick ) + { + MouseEvent aMEvt = rTEvt.GetMouseEvent(); + Point aPt( aMEvt.GetPosPixel() ); + bool bShift = aMEvt.IsShift(); + bool bCtrl = aMEvt.IsMod1(); + + sal_Int32 nSelect = LISTBOX_ENTRY_NOTFOUND; + if( aPt.Y() < 0 ) + { + if ( mnCurrentPos != LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = mnCurrentPos ? ( mnCurrentPos - 1 ) : 0; + if( nSelect < mnTop ) + SetTopEntry( mnTop-1 ); + } + } + else if( aPt.Y() > GetOutputSizePixel().Height() ) + { + if ( mnCurrentPos != LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = std::min( static_cast<sal_Int32>(mnCurrentPos+1), static_cast<sal_Int32>(maEntryList.GetEntryCount()-1) ); + if( nSelect >= GetLastVisibleEntry() ) + SetTopEntry( mnTop+1 ); + } + } + else + { + nSelect = static_cast<sal_Int32>( ( aPt.Y() + gnBorder ) / mnMaxHeight ) + mnTop; + nSelect = std::min( nSelect, GetLastVisibleEntry() ); + nSelect = std::min( nSelect, static_cast<sal_Int32>( maEntryList.GetEntryCount() - 1 ) ); + } + + if ( bInside ) + { + if ( ( nSelect != mnCurrentPos ) || !GetEntryList().GetSelectedEntryCount() ) + { + mbTrackingSelect = true; + SelectEntries(nSelect, LET_TRACKING, bShift, bCtrl); + mbTrackingSelect = false; + } + } + else + { + if ( !mbMulti && GetEntryList().GetSelectedEntryCount() ) + { + mbTrackingSelect = true; + SelectEntry( GetEntryList().GetSelectedEntryPos( 0 ), false ); + mbTrackingSelect = false; + } + } + mnCurrentPos = nSelect; + if ( mnCurrentPos == LISTBOX_ENTRY_NOTFOUND ) + { + ImplHideFocusRect(); + } + else + { + tools::Long nHeightDiff = maEntryList.GetAddedHeight( mnCurrentPos, mnTop ); + maFocusRect.SetPos( Point( 0, nHeightDiff ) ); + Size aSz( maFocusRect.GetWidth(), maEntryList.GetEntryHeight( mnCurrentPos ) ); + maFocusRect.SetSize( aSz ); + ImplShowFocusRect(); + } + } + } +} + +void ImplListBoxWindow::KeyInput( const KeyEvent& rKEvt ) +{ + if( !ProcessKeyInput( rKEvt ) ) + Control::KeyInput( rKEvt ); +} + +bool ImplListBoxWindow::ProcessKeyInput( const KeyEvent& rKEvt ) +{ + // entry to be selected + sal_Int32 nSelect = LISTBOX_ENTRY_NOTFOUND; + LB_EVENT_TYPE eLET = LET_KEYMOVE; + + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + bool bShift = aKeyCode.IsShift(); + bool bCtrl = aKeyCode.IsMod1() || aKeyCode.IsMod3(); + bool bMod2 = aKeyCode.IsMod2(); + bool bDone = false; + bool bHandleKey = false; + + switch( aKeyCode.GetCode() ) + { + case KEY_UP: + { + if ( IsReadOnly() ) + { + if ( GetTopEntry() ) + SetTopEntry( GetTopEntry()-1 ); + } + else if ( !bMod2 ) + { + if( mnCurrentPos == LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = maEntryList.FindFirstSelectable( 0 ); + } + else if ( mnCurrentPos ) + { + // search first selectable above the current position + nSelect = maEntryList.FindFirstSelectable( mnCurrentPos - 1, false ); + } + + if( ( nSelect != LISTBOX_ENTRY_NOTFOUND ) && ( nSelect < mnTop ) ) + SetTopEntry( mnTop-1 ); + + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_DOWN: + { + if ( IsReadOnly() ) + { + SetTopEntry( GetTopEntry()+1 ); + } + else if ( !bMod2 ) + { + if( mnCurrentPos == LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = maEntryList.FindFirstSelectable( 0 ); + } + else if ( (mnCurrentPos+1) < maEntryList.GetEntryCount() ) + { + // search first selectable below the current position + nSelect = maEntryList.FindFirstSelectable( mnCurrentPos + 1 ); + } + + if( ( nSelect != LISTBOX_ENTRY_NOTFOUND ) && ( nSelect >= GetLastVisibleEntry() ) ) + SetTopEntry( mnTop+1 ); + + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_PAGEUP: + { + if ( IsReadOnly() ) + { + sal_Int32 nCurVis = GetLastVisibleEntry() - mnTop +1; + SetTopEntry( ( mnTop > nCurVis ) ? + (mnTop-nCurVis) : 0 ); + } + else if ( !bCtrl && !bMod2 ) + { + if( mnCurrentPos == LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = maEntryList.FindFirstSelectable( 0 ); + } + else if ( mnCurrentPos ) + { + if( mnCurrentPos == mnTop ) + { + sal_Int32 nCurVis = GetLastVisibleEntry() - mnTop +1; + SetTopEntry( ( mnTop > nCurVis ) ? ( mnTop-nCurVis+1 ) : 0 ); + } + + // find first selectable starting from mnTop looking forward + nSelect = maEntryList.FindFirstSelectable( mnTop ); + } + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_PAGEDOWN: + { + if ( IsReadOnly() ) + { + SetTopEntry( GetLastVisibleEntry() ); + } + else if ( !bCtrl && !bMod2 ) + { + if( mnCurrentPos == LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = maEntryList.FindFirstSelectable( 0 ); + } + else if ( (mnCurrentPos+1) < maEntryList.GetEntryCount() ) + { + sal_Int32 nCount = maEntryList.GetEntryCount(); + sal_Int32 nCurVis = GetLastVisibleEntry() - mnTop; + sal_Int32 nTmp = std::min( nCurVis, nCount ); + nTmp += mnTop - 1; + if( mnCurrentPos == nTmp && mnCurrentPos != nCount - 1 ) + { + tools::Long nTmp2 = std::min( static_cast<tools::Long>(nCount-nCurVis), static_cast<tools::Long>(static_cast<tools::Long>(mnTop)+static_cast<tools::Long>(nCurVis)-1) ); + nTmp2 = std::max( tools::Long(0) , nTmp2 ); + nTmp = static_cast<sal_Int32>(nTmp2+(nCurVis-1) ); + SetTopEntry( static_cast<sal_Int32>(nTmp2) ); + } + // find first selectable starting from nTmp looking backwards + nSelect = maEntryList.FindFirstSelectable( nTmp, false ); + } + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_HOME: + { + if ( IsReadOnly() ) + { + SetTopEntry( 0 ); + } + else if ( !bCtrl && !bMod2 && mnCurrentPos ) + { + nSelect = maEntryList.FindFirstSelectable( maEntryList.GetEntryCount() ? 0 : LISTBOX_ENTRY_NOTFOUND ); + if( mnTop != 0 ) + SetTopEntry( 0 ); + + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_END: + { + if ( IsReadOnly() ) + { + SetTopEntry( 0xFFFF ); + } + else if ( !bCtrl && !bMod2 ) + { + if( mnCurrentPos == LISTBOX_ENTRY_NOTFOUND ) + { + nSelect = maEntryList.FindFirstSelectable( 0 ); + } + else if ( (mnCurrentPos+1) < maEntryList.GetEntryCount() ) + { + sal_Int32 nCount = maEntryList.GetEntryCount(); + nSelect = maEntryList.FindFirstSelectable( nCount - 1, false ); + sal_Int32 nCurVis = GetLastVisibleEntry() - mnTop + 1; + if( nCount > nCurVis ) + SetTopEntry( nCount - nCurVis ); + } + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_LEFT: + { + if ( !bCtrl && !bMod2 ) + { + ScrollHorz( -HORZ_SCROLL ); + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_RIGHT: + { + if ( !bCtrl && !bMod2 ) + { + ScrollHorz( HORZ_SCROLL ); + bDone = true; + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_RETURN: + { + if ( !bMod2 && !IsReadOnly() ) + { + mnSelectModifier = rKEvt.GetKeyCode().GetModifier(); + ImplCallSelect(); + bDone = false; // do not catch RETURN + } + maQuickSelectionEngine.Reset(); + } + break; + + case KEY_SPACE: + { + if ( !bMod2 && !IsReadOnly() ) + { + if( mbMulti && ( !mbSimpleMode || ( mbSimpleMode && bCtrl && !bShift ) ) ) + { + nSelect = mnCurrentPos; + eLET = LET_KEYSPACE; + } + bDone = true; + } + bHandleKey = true; + } + break; + + case KEY_A: + { + if( bCtrl && mbMulti ) + { + // paint only once + bool bUpdates = IsUpdateMode(); + SetUpdateMode( false ); + + sal_Int32 nEntryCount = maEntryList.GetEntryCount(); + for( sal_Int32 i = 0; i < nEntryCount; i++ ) + SelectEntry( i, true ); + + // tdf#97066 - Update selected items + ImplCallSelect(); + + // restore update mode + SetUpdateMode( bUpdates ); + Invalidate(); + + maQuickSelectionEngine.Reset(); + + bDone = true; + } + else + { + bHandleKey = true; + } + } + break; + + default: + bHandleKey = true; + break; + } + if (bHandleKey && !IsReadOnly()) + { + bDone = maQuickSelectionEngine.HandleKeyEvent( rKEvt ); + } + + if ( ( nSelect != LISTBOX_ENTRY_NOTFOUND ) + && ( ( !maEntryList.IsEntryPosSelected( nSelect ) ) + || ( eLET == LET_KEYSPACE ) + ) + ) + { + SAL_WARN_IF( maEntryList.IsEntryPosSelected( nSelect ) && !mbMulti, "vcl", "ImplListBox: Selecting same Entry" ); + sal_Int32 nCount = maEntryList.GetEntryCount(); + if (nSelect >= nCount) + nSelect = nCount ? nCount-1 : LISTBOX_ENTRY_NOTFOUND; + bool bCurPosChange = (mnCurrentPos != nSelect); + mnCurrentPos = nSelect; + if(SelectEntries( nSelect, eLET, bShift, bCtrl, bCurPosChange)) + { + // tdf#129043 Correctly deliver events when changing values with arrow keys in combobox + if (mbIsDropdown && IsReallyVisible()) + mbTravelSelect = true; + mnSelectModifier = rKEvt.GetKeyCode().GetModifier(); + ImplCallSelect(); + mbTravelSelect = false; + } + } + + return bDone; +} + +namespace +{ + vcl::StringEntryIdentifier lcl_getEntry( const ImplEntryList& _rList, sal_Int32 _nPos, OUString& _out_entryText ) + { + OSL_PRECOND( ( _nPos != LISTBOX_ENTRY_NOTFOUND ), "lcl_getEntry: invalid position!" ); + sal_Int32 nEntryCount( _rList.GetEntryCount() ); + if ( _nPos >= nEntryCount ) + _nPos = 0; + _out_entryText = _rList.GetEntryText( _nPos ); + + // vcl::StringEntryIdentifier does not allow for 0 values, but our position is 0-based + // => normalize + return reinterpret_cast< vcl::StringEntryIdentifier >( _nPos + 1 ); + } + + sal_Int32 lcl_getEntryPos( vcl::StringEntryIdentifier _entry ) + { + // our pos is 0-based, but StringEntryIdentifier does not allow for a NULL + return static_cast< sal_Int32 >( reinterpret_cast< sal_Int64 >( _entry ) ) - 1; + } +} + +vcl::StringEntryIdentifier ImplListBoxWindow::CurrentEntry( OUString& _out_entryText ) const +{ + return lcl_getEntry( GetEntryList(), ( mnCurrentPos == LISTBOX_ENTRY_NOTFOUND ) ? 0 : mnCurrentPos, _out_entryText ); +} + +vcl::StringEntryIdentifier ImplListBoxWindow::NextEntry( vcl::StringEntryIdentifier _currentEntry, OUString& _out_entryText ) const +{ + sal_Int32 nNextPos = lcl_getEntryPos( _currentEntry ) + 1; + return lcl_getEntry( GetEntryList(), nNextPos, _out_entryText ); +} + +void ImplListBoxWindow::SelectEntry( vcl::StringEntryIdentifier _entry ) +{ + sal_Int32 nSelect = lcl_getEntryPos( _entry ); + if ( maEntryList.IsEntryPosSelected( nSelect ) ) + { + // ignore that. This method is a callback from the QuickSelectionEngine, which means the user attempted + // to select the given entry by typing its starting letters. No need to act. + return; + } + + // normalize + OSL_ENSURE( nSelect < maEntryList.GetEntryCount(), "ImplListBoxWindow::SelectEntry: how that?" ); + sal_Int32 nCount = maEntryList.GetEntryCount(); + if (nSelect >= nCount) + nSelect = nCount ? nCount-1 : LISTBOX_ENTRY_NOTFOUND; + + // make visible + ShowProminentEntry( nSelect ); + + // actually select + mnCurrentPos = nSelect; + if ( SelectEntries( nSelect, LET_KEYMOVE ) ) + { + mbTravelSelect = true; + mnSelectModifier = 0; + ImplCallSelect(); + mbTravelSelect = false; + } +} + +void ImplListBoxWindow::ImplPaint(vcl::RenderContext& rRenderContext, sal_Int32 nPos) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + const ImplEntryType* pEntry = maEntryList.GetEntryPtr( nPos ); + if (!pEntry) + return; + + tools::Long nWidth = GetOutputSizePixel().Width(); + tools::Long nY = maEntryList.GetAddedHeight(nPos, mnTop); + tools::Rectangle aRect(Point(0, nY), Size(nWidth, pEntry->getHeightWithMargin())); + + bool bSelected = maEntryList.IsEntryPosSelected(nPos); + if (bSelected) + { + rRenderContext.SetTextColor(!IsEnabled() ? rStyleSettings.GetDisableColor() : rStyleSettings.GetHighlightTextColor()); + rRenderContext.SetFillColor(rStyleSettings.GetHighlightColor()); + rRenderContext.SetLineColor(); + rRenderContext.DrawRect(aRect); + } + else + { + ApplySettings(rRenderContext); + if (!IsEnabled()) + rRenderContext.SetTextColor(rStyleSettings.GetDisableColor()); + } + rRenderContext.SetTextFillColor(); + + if (IsUserDrawEnabled()) + { + mbInUserDraw = true; + mnUserDrawEntry = nPos; + aRect.AdjustLeft( -mnLeft ); + if (nPos < GetEntryList().GetMRUCount()) + nPos = GetEntryList().FindEntry(GetEntryList().GetEntryText(nPos)); + nPos = nPos - GetEntryList().GetMRUCount(); + + UserDrawEvent aUDEvt(&rRenderContext, aRect, nPos, bSelected); + maUserDrawHdl.Call( &aUDEvt ); + mbInUserDraw = false; + } + else + { + DrawEntry(rRenderContext, nPos, true, true); + } +} + +void ImplListBoxWindow::DrawEntry(vcl::RenderContext& rRenderContext, sal_Int32 nPos, bool bDrawImage, bool bDrawText) +{ + const ImplEntryType* pEntry = maEntryList.GetEntryPtr(nPos); + if (!pEntry) + return; + + tools::Long nEntryHeight = pEntry->getHeightWithMargin(); + + // when changing this function don't forget to adjust ImplWin::DrawEntry() + + if (mbInUserDraw) + nPos = mnUserDrawEntry; // real entry, not the matching entry from MRU + + tools::Long nY = maEntryList.GetAddedHeight(nPos, mnTop); + + if (bDrawImage && maEntryList.HasImages()) + { + Image aImage = maEntryList.GetEntryImage(nPos); + if (!!aImage) + { + Size aImgSz = aImage.GetSizePixel(); + Point aPtImg(gnBorder - mnLeft, nY + ((nEntryHeight - aImgSz.Height()) / 2)); + + if (!IsZoom()) + { + rRenderContext.DrawImage(aPtImg, aImage); + } + else + { + aImgSz.setWidth( CalcZoom(aImgSz.Width()) ); + aImgSz.setHeight( CalcZoom(aImgSz.Height()) ); + rRenderContext.DrawImage(aPtImg, aImgSz, aImage); + } + + const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); + const sal_uInt16 nEdgeBlendingPercent(GetEdgeBlending() ? rStyleSettings.GetEdgeBlending() : 0); + + if (nEdgeBlendingPercent && aImgSz.Width() && aImgSz.Height()) + { + const Color& rTopLeft(rStyleSettings.GetEdgeBlendingTopLeftColor()); + const Color& rBottomRight(rStyleSettings.GetEdgeBlendingBottomRightColor()); + const sal_uInt8 nAlpha((nEdgeBlendingPercent * 255) / 100); + const BitmapEx aBlendFrame(createBlendFrame(aImgSz, nAlpha, rTopLeft, rBottomRight)); + + if (!aBlendFrame.IsEmpty()) + { + rRenderContext.DrawBitmapEx(aPtImg, aBlendFrame); + } + } + } + } + + if (bDrawText) + { + OUString aStr(maEntryList.GetEntryText(nPos)); + if (!aStr.isEmpty()) + { + tools::Long nMaxWidth = std::max(mnMaxWidth, GetOutputSizePixel().Width() - 2 * gnBorder); + // a multiline entry should only be as wide as the window + if (pEntry->mnFlags & ListBoxEntryFlags::MultiLine) + nMaxWidth = GetOutputSizePixel().Width() - 2 * gnBorder; + + tools::Rectangle aTextRect(Point(gnBorder - mnLeft, nY), + Size(nMaxWidth, nEntryHeight)); + + if (maEntryList.HasEntryImage(nPos) || IsUserDrawEnabled()) + { + tools::Long nImageWidth = std::max(mnMaxImgWidth, maUserItemSize.Width()); + aTextRect.AdjustLeft(nImageWidth + IMG_TXT_DISTANCE ); + } + + DrawTextFlags nDrawStyle = ImplGetTextStyle(); + if (pEntry->mnFlags & ListBoxEntryFlags::MultiLine) + nDrawStyle |= MULTILINE_ENTRY_DRAW_FLAGS; + if (pEntry->mnFlags & ListBoxEntryFlags::DrawDisabled) + nDrawStyle |= DrawTextFlags::Disable; + + rRenderContext.DrawText(aTextRect, aStr, nDrawStyle); + } + } + + if ( !maSeparators.empty() && ( isSeparator(nPos) || isSeparator(nPos-1) ) ) + { + Color aOldLineColor(rRenderContext.GetLineColor()); + rRenderContext.SetLineColor((GetBackground() != COL_LIGHTGRAY) ? COL_LIGHTGRAY : COL_GRAY); + Point aStartPos(0, nY); + if (isSeparator(nPos)) + aStartPos.AdjustY(pEntry->getHeightWithMargin() - 1 ); + Point aEndPos(aStartPos); + aEndPos.setX( GetOutputSizePixel().Width() ); + rRenderContext.DrawLine(aStartPos, aEndPos); + rRenderContext.SetLineColor(aOldLineColor); + } +} + +void ImplListBoxWindow::FillLayoutData() const +{ + mxLayoutData.emplace(); + const_cast<ImplListBoxWindow*>(this)->Invalidate(tools::Rectangle(Point(0, 0), GetOutDev()->GetOutputSize())); +} + +void ImplListBoxWindow::ImplDoPaint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + sal_Int32 nCount = maEntryList.GetEntryCount(); + + bool bShowFocusRect = mbHasFocusRect; + if (mbHasFocusRect) + ImplHideFocusRect(); + + tools::Long nY = 0; // + gnBorder; + tools::Long nHeight = GetOutputSizePixel().Height();// - mnMaxHeight + gnBorder; + + for (sal_Int32 i = mnTop; i < nCount && nY < nHeight + mnMaxHeight; i++) + { + const ImplEntryType* pEntry = maEntryList.GetEntryPtr(i); + tools::Long nEntryHeight = pEntry->getHeightWithMargin(); + if (nY + nEntryHeight >= rRect.Top() && + nY <= rRect.Bottom() + mnMaxHeight) + { + ImplPaint(rRenderContext, i); + } + nY += nEntryHeight; + } + + tools::Long nHeightDiff = maEntryList.GetAddedHeight(mnCurrentPos, mnTop); + maFocusRect.SetPos(Point(0, nHeightDiff)); + Size aSz(maFocusRect.GetWidth(), maEntryList.GetEntryHeight(mnCurrentPos)); + maFocusRect.SetSize(aSz); + if (HasFocus() && bShowFocusRect) + ImplShowFocusRect(); +} + +void ImplListBoxWindow::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + if (SupportsDoubleBuffering()) + { + // This widget is explicitly double-buffered, so avoid partial paints. + tools::Rectangle aRect(Point(0, 0), GetOutputSizePixel()); + ImplDoPaint(rRenderContext, aRect); + } + else + ImplDoPaint(rRenderContext, rRect); +} + +sal_uInt16 ImplListBoxWindow::GetDisplayLineCount() const +{ + // FIXME: ListBoxEntryFlags::MultiLine + + const sal_Int32 nCount = maEntryList.GetEntryCount()-mnTop; + tools::Long nHeight = GetOutputSizePixel().Height();// - mnMaxHeight + gnBorder; + sal_uInt16 nEntries = static_cast< sal_uInt16 >( ( nHeight + mnMaxHeight - 1 ) / mnMaxHeight ); + if( nEntries > nCount ) + nEntries = static_cast<sal_uInt16>(nCount); + + return nEntries; +} + +void ImplListBoxWindow::Resize() +{ + Control::Resize(); + + bool bShowFocusRect = mbHasFocusRect; + if ( bShowFocusRect ) + ImplHideFocusRect(); + + if( mnCurrentPos != LISTBOX_ENTRY_NOTFOUND ) + { + Size aSz( GetOutputSizePixel().Width(), maEntryList.GetEntryHeight( mnCurrentPos ) ); + maFocusRect.SetSize( aSz ); + } + + if ( bShowFocusRect ) + ImplShowFocusRect(); + + ImplClearLayoutData(); +} + +void ImplListBoxWindow::GetFocus() +{ + sal_Int32 nPos = mnCurrentPos; + if ( nPos == LISTBOX_ENTRY_NOTFOUND ) + nPos = 0; + tools::Long nHeightDiff = maEntryList.GetAddedHeight( nPos, mnTop ); + maFocusRect.SetPos( Point( 0, nHeightDiff ) ); + Size aSz( maFocusRect.GetWidth(), maEntryList.GetEntryHeight( nPos ) ); + maFocusRect.SetSize( aSz ); + ImplShowFocusRect(); + Control::GetFocus(); +} + +void ImplListBoxWindow::LoseFocus() +{ + ImplHideFocusRect(); + Control::LoseFocus(); +} + +void ImplListBoxWindow::SetTopEntry( sal_Int32 nTop ) +{ + if( maEntryList.GetEntryCount() == 0 ) + return; + + tools::Long nWHeight = PixelToLogic( GetSizePixel() ).Height(); + + sal_Int32 nLastEntry = maEntryList.GetEntryCount()-1; + if( nTop > nLastEntry ) + nTop = nLastEntry; + const ImplEntryType* pLast = maEntryList.GetEntryPtr( nLastEntry ); + while( nTop > 0 && maEntryList.GetAddedHeight( nLastEntry, nTop-1 ) + pLast->getHeightWithMargin() <= nWHeight ) + nTop--; + + if ( nTop == mnTop ) + return; + + ImplClearLayoutData(); + tools::Long nDiff = maEntryList.GetAddedHeight( mnTop, nTop ); + PaintImmediately(); + ImplHideFocusRect(); + mnTop = nTop; + Scroll( 0, nDiff ); + PaintImmediately(); + if( HasFocus() ) + ImplShowFocusRect(); + maScrollHdl.Call( this ); +} + +void ImplListBoxWindow::ShowProminentEntry( sal_Int32 nEntryPos ) +{ + sal_Int32 nPos = nEntryPos; + auto nWHeight = PixelToLogic( GetSizePixel() ).Height(); + while( nEntryPos > 0 && maEntryList.GetAddedHeight( nPos+1, nEntryPos ) < nWHeight/2 ) + nEntryPos--; + + SetTopEntry( nEntryPos ); +} + +void ImplListBoxWindow::SetLeftIndent( tools::Long n ) +{ + ScrollHorz( n - mnLeft ); +} + +void ImplListBoxWindow::ScrollHorz( tools::Long n ) +{ + tools::Long nDiff = 0; + if ( n > 0 ) + { + tools::Long nWidth = GetOutputSizePixel().Width(); + if( ( mnMaxWidth - mnLeft + n ) > nWidth ) + nDiff = n; + } + else if ( n < 0 ) + { + if( mnLeft ) + { + tools::Long nAbs = -n; + nDiff = - std::min( mnLeft, nAbs ); + } + } + + if ( nDiff ) + { + ImplClearLayoutData(); + mnLeft = sal::static_int_cast<sal_uInt16>(mnLeft + nDiff); + PaintImmediately(); + ImplHideFocusRect(); + Scroll( -nDiff, 0 ); + PaintImmediately(); + if( HasFocus() ) + ImplShowFocusRect(); + maScrollHdl.Call( this ); + } +} + +void ImplListBoxWindow::SetSeparatorPos( sal_Int32 n ) +{ + maSeparators.clear(); + + if ( n != LISTBOX_ENTRY_NOTFOUND ) + { + maSeparators.insert( n ); + } +} + +sal_Int32 ImplListBoxWindow::GetSeparatorPos() const +{ + if (!maSeparators.empty()) + return *(maSeparators.begin()); + else + return LISTBOX_ENTRY_NOTFOUND; +} + +bool ImplListBoxWindow::isSeparator( const sal_Int32 &n) const +{ + return maSeparators.find(n) != maSeparators.end(); +} + +Size ImplListBoxWindow::CalcSize(sal_Int32 nMaxLines) const +{ + // FIXME: ListBoxEntryFlags::MultiLine + + Size aSz; + aSz.setHeight(nMaxLines * GetEntryHeightWithMargin()); + aSz.setWidth( mnMaxWidth + 2*gnBorder ); + return aSz; +} + +tools::Rectangle ImplListBoxWindow::GetBoundingRectangle( sal_Int32 nItem ) const +{ + const ImplEntryType* pEntry = maEntryList.GetEntryPtr( nItem ); + Size aSz( GetSizePixel().Width(), pEntry ? pEntry->getHeightWithMargin() : GetEntryHeightWithMargin() ); + tools::Long nY = maEntryList.GetAddedHeight( nItem, GetTopEntry() ) + GetEntryList().GetMRUCount()*GetEntryHeightWithMargin(); + tools::Rectangle aRect( Point( 0, nY ), aSz ); + return aRect; +} + +void ImplListBoxWindow::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( nType == StateChangedType::Zoom ) + { + ApplySettings(*GetOutDev()); + ImplCalcMetrics(); + Invalidate(); + } + else if ( nType == StateChangedType::UpdateMode ) + { + if ( IsUpdateMode() && IsReallyVisible() ) + Invalidate(); + } + else if ( nType == StateChangedType::ControlFont ) + { + ApplySettings(*GetOutDev()); + ImplCalcMetrics(); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ApplySettings(*GetOutDev()); + Invalidate(); + } + else if( nType == StateChangedType::Enable ) + { + Invalidate(); + } + + ImplClearLayoutData(); +} + +void ImplListBoxWindow::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ImplClearLayoutData(); + ApplySettings(*GetOutDev()); + ImplCalcMetrics(); + Invalidate(); + } +} + +DrawTextFlags ImplListBoxWindow::ImplGetTextStyle() const +{ + DrawTextFlags nTextStyle = DrawTextFlags::VCenter; + + if (maEntryList.HasImages()) + nTextStyle |= DrawTextFlags::Left; + else if (mbCenter) + nTextStyle |= DrawTextFlags::Center; + else if (mbRight) + nTextStyle |= DrawTextFlags::Right; + else + nTextStyle |= DrawTextFlags::Left; + + return nTextStyle; +} + +ImplListBox::ImplListBox( vcl::Window* pParent, WinBits nWinStyle ) : + Control( pParent, nWinStyle ), + maLBWindow(VclPtr<ImplListBoxWindow>::Create( this, nWinStyle&(~WB_BORDER) )) +{ + // for native widget rendering we must be able to detect this window type + SetType( WindowType::LISTBOXWINDOW ); + + mpVScrollBar = VclPtr<ScrollBar>::Create( this, WB_VSCROLL | WB_DRAG ); + mpHScrollBar = VclPtr<ScrollBar>::Create( this, WB_HSCROLL | WB_DRAG ); + mpScrollBarBox = VclPtr<ScrollBarBox>::Create( this ); + + Link<ScrollBar*,void> aLink( LINK( this, ImplListBox, ScrollBarHdl ) ); + mpVScrollBar->SetScrollHdl( aLink ); + mpHScrollBar->SetScrollHdl( aLink ); + + mbVScroll = false; + mbHScroll = false; + mbAutoHScroll = ( nWinStyle & WB_AUTOHSCROLL ); + mbEdgeBlending = false; + + maLBWindow->SetScrollHdl( LINK( this, ImplListBox, LBWindowScrolled ) ); + maLBWindow->SetMRUChangedHdl( LINK( this, ImplListBox, MRUChanged ) ); + maLBWindow->SetEdgeBlending(GetEdgeBlending()); + maLBWindow->Show(); +} + +ImplListBox::~ImplListBox() +{ + disposeOnce(); +} + +void ImplListBox::dispose() +{ + mpHScrollBar.disposeAndClear(); + mpVScrollBar.disposeAndClear(); + mpScrollBarBox.disposeAndClear(); + maLBWindow.disposeAndClear(); + Control::dispose(); +} + +void ImplListBox::Clear() +{ + maLBWindow->Clear(); + if ( GetEntryList().GetMRUCount() ) + { + maLBWindow->GetEntryList().SetMRUCount( 0 ); + maLBWindow->SetSeparatorPos( LISTBOX_ENTRY_NOTFOUND ); + } + mpVScrollBar->SetThumbPos( 0 ); + mpHScrollBar->SetThumbPos( 0 ); + CompatStateChanged( StateChangedType::Data ); +} + +sal_Int32 ImplListBox::InsertEntry( sal_Int32 nPos, const OUString& rStr ) +{ + ImplEntryType* pNewEntry = new ImplEntryType( rStr ); + sal_Int32 nNewPos = maLBWindow->InsertEntry( nPos, pNewEntry ); + CompatStateChanged( StateChangedType::Data ); + return nNewPos; +} + +sal_Int32 ImplListBox::InsertEntry( sal_Int32 nPos, const OUString& rStr, const Image& rImage ) +{ + ImplEntryType* pNewEntry = new ImplEntryType( rStr, rImage ); + sal_Int32 nNewPos = maLBWindow->InsertEntry( nPos, pNewEntry ); + CompatStateChanged( StateChangedType::Data ); + return nNewPos; +} + +void ImplListBox::RemoveEntry( sal_Int32 nPos ) +{ + maLBWindow->RemoveEntry( nPos ); + CompatStateChanged( StateChangedType::Data ); +} + +void ImplListBox::SetEntryFlags( sal_Int32 nPos, ListBoxEntryFlags nFlags ) +{ + maLBWindow->SetEntryFlags( nPos, nFlags ); +} + +void ImplListBox::SelectEntry( sal_Int32 nPos, bool bSelect ) +{ + maLBWindow->SelectEntry( nPos, bSelect ); +} + +void ImplListBox::SetNoSelection() +{ + maLBWindow->DeselectAll(); +} + +void ImplListBox::GetFocus() +{ + if (maLBWindow) + maLBWindow->GrabFocus(); + else + Control::GetFocus(); +} + +void ImplListBox::Resize() +{ + Control::Resize(); + ImplResizeControls(); + ImplCheckScrollBars(); +} + +IMPL_LINK_NOARG(ImplListBox, MRUChanged, LinkParamNone*, void) +{ + CompatStateChanged( StateChangedType::Data ); +} + +IMPL_LINK_NOARG(ImplListBox, LBWindowScrolled, ImplListBoxWindow*, void) +{ + tools::Long nSet = GetTopEntry(); + if( nSet > mpVScrollBar->GetRangeMax() ) + mpVScrollBar->SetRangeMax( GetEntryList().GetEntryCount() ); + mpVScrollBar->SetThumbPos( GetTopEntry() ); + + mpHScrollBar->SetThumbPos( GetLeftIndent() ); + + maScrollHdl.Call( this ); +} + +IMPL_LINK( ImplListBox, ScrollBarHdl, ScrollBar*, pSB, void ) +{ + sal_uInt16 nPos = static_cast<sal_uInt16>(pSB->GetThumbPos()); + if( pSB == mpVScrollBar ) + SetTopEntry( nPos ); + else if( pSB == mpHScrollBar ) + SetLeftIndent( nPos ); + if( GetParent() ) + GetParent()->Invalidate( InvalidateFlags::Update ); +} + +void ImplListBox::ImplCheckScrollBars() +{ + bool bArrange = false; + + Size aOutSz = GetOutputSizePixel(); + sal_Int32 nEntries = GetEntryList().GetEntryCount(); + sal_uInt16 nMaxVisEntries = static_cast<sal_uInt16>(aOutSz.Height() / GetEntryHeightWithMargin()); + + // vertical ScrollBar + if( nEntries > nMaxVisEntries ) + { + if( !mbVScroll ) + bArrange = true; + mbVScroll = true; + + // check of the scrolled-out region + if( GetEntryList().GetSelectedEntryCount() == 1 && + GetEntryList().GetSelectedEntryPos( 0 ) != LISTBOX_ENTRY_NOTFOUND ) + ShowProminentEntry( GetEntryList().GetSelectedEntryPos( 0 ) ); + else + SetTopEntry( GetTopEntry() ); // MaxTop is being checked... + } + else + { + if( mbVScroll ) + bArrange = true; + mbVScroll = false; + SetTopEntry( 0 ); + } + + // horizontal ScrollBar + if( mbAutoHScroll ) + { + tools::Long nWidth = static_cast<sal_uInt16>(aOutSz.Width()); + if ( mbVScroll ) + nWidth -= mpVScrollBar->GetSizePixel().Width(); + + tools::Long nMaxWidth = GetMaxEntryWidth(); + if( nWidth < nMaxWidth ) + { + if( !mbHScroll ) + bArrange = true; + mbHScroll = true; + + if ( !mbVScroll ) // maybe we do need one now + { + nMaxVisEntries = static_cast<sal_uInt16>( ( aOutSz.Height() - mpHScrollBar->GetSizePixel().Height() ) / GetEntryHeightWithMargin() ); + if( nEntries > nMaxVisEntries ) + { + bArrange = true; + mbVScroll = true; + + // check of the scrolled-out region + if( GetEntryList().GetSelectedEntryCount() == 1 && + GetEntryList().GetSelectedEntryPos( 0 ) != LISTBOX_ENTRY_NOTFOUND ) + ShowProminentEntry( GetEntryList().GetSelectedEntryPos( 0 ) ); + else + SetTopEntry( GetTopEntry() ); // MaxTop is being checked... + } + } + + // check of the scrolled-out region + sal_uInt16 nMaxLI = static_cast<sal_uInt16>(nMaxWidth - nWidth); + if ( nMaxLI < GetLeftIndent() ) + SetLeftIndent( nMaxLI ); + } + else + { + if( mbHScroll ) + bArrange = true; + mbHScroll = false; + SetLeftIndent( 0 ); + } + } + + if( bArrange ) + ImplResizeControls(); + + ImplInitScrollBars(); +} + +void ImplListBox::ImplInitScrollBars() +{ + Size aOutSz = maLBWindow->GetOutputSizePixel(); + + if ( mbVScroll ) + { + sal_Int32 nEntries = GetEntryList().GetEntryCount(); + sal_uInt16 nVisEntries = static_cast<sal_uInt16>(aOutSz.Height() / GetEntryHeightWithMargin()); + mpVScrollBar->SetRangeMax( nEntries ); + mpVScrollBar->SetVisibleSize( nVisEntries ); + mpVScrollBar->SetPageSize( nVisEntries - 1 ); + } + + if ( mbHScroll ) + { + mpHScrollBar->SetRangeMax( GetMaxEntryWidth() + HORZ_SCROLL ); + mpHScrollBar->SetVisibleSize( static_cast<sal_uInt16>(aOutSz.Width()) ); + mpHScrollBar->SetLineSize( HORZ_SCROLL ); + mpHScrollBar->SetPageSize( aOutSz.Width() - HORZ_SCROLL ); + } +} + +void ImplListBox::ImplResizeControls() +{ + // Here we only position the Controls; if the Scrollbars are to be + // visible is already determined in ImplCheckScrollBars + + Size aOutSz = GetOutputSizePixel(); + tools::Long nSBWidth = GetSettings().GetStyleSettings().GetScrollBarSize(); + nSBWidth = CalcZoom( nSBWidth ); + + Size aInnerSz( aOutSz ); + if ( mbVScroll ) + aInnerSz.AdjustWidth( -nSBWidth ); + if ( mbHScroll ) + aInnerSz.AdjustHeight( -nSBWidth ); + + Point aWinPos( 0, 0 ); + maLBWindow->SetPosSizePixel( aWinPos, aInnerSz ); + + // ScrollBarBox + if( mbVScroll && mbHScroll ) + { + Point aBoxPos( aInnerSz.Width(), aInnerSz.Height() ); + mpScrollBarBox->SetPosSizePixel( aBoxPos, Size( nSBWidth, nSBWidth ) ); + mpScrollBarBox->Show(); + } + else + { + mpScrollBarBox->Hide(); + } + + // vertical ScrollBar + if( mbVScroll ) + { + // Scrollbar on left or right side? + Point aVPos( aOutSz.Width() - nSBWidth, 0 ); + mpVScrollBar->SetPosSizePixel( aVPos, Size( nSBWidth, aInnerSz.Height() ) ); + mpVScrollBar->Show(); + } + else + { + mpVScrollBar->Hide(); + // #107254# Don't reset top entry after resize, but check for max top entry + SetTopEntry( GetTopEntry() ); + } + + // horizontal ScrollBar + if( mbHScroll ) + { + Point aHPos( 0, aOutSz.Height() - nSBWidth ); + mpHScrollBar->SetPosSizePixel( aHPos, Size( aInnerSz.Width(), nSBWidth ) ); + mpHScrollBar->Show(); + } + else + { + mpHScrollBar->Hide(); + SetLeftIndent( 0 ); + } +} + +void ImplListBox::StateChanged( StateChangedType nType ) +{ + if ( nType == StateChangedType::InitShow ) + { + ImplCheckScrollBars(); + } + else if ( ( nType == StateChangedType::UpdateMode ) || ( nType == StateChangedType::Data ) ) + { + bool bUpdate = IsUpdateMode(); + maLBWindow->SetUpdateMode( bUpdate ); + if ( bUpdate && IsReallyVisible() ) + ImplCheckScrollBars(); + } + else if( nType == StateChangedType::Enable ) + { + mpHScrollBar->Enable( IsEnabled() ); + mpVScrollBar->Enable( IsEnabled() ); + mpScrollBarBox->Enable( IsEnabled() ); + maLBWindow->Enable( IsEnabled() ); + + Invalidate(); + } + else if ( nType == StateChangedType::Zoom ) + { + maLBWindow->SetZoom( GetZoom() ); + Resize(); + } + else if ( nType == StateChangedType::ControlFont ) + { + maLBWindow->SetControlFont( GetControlFont() ); + } + else if ( nType == StateChangedType::ControlForeground ) + { + maLBWindow->SetControlForeground( GetControlForeground() ); + } + else if ( nType == StateChangedType::ControlBackground ) + { + maLBWindow->SetControlBackground( GetControlBackground() ); + } + else if( nType == StateChangedType::Mirroring ) + { + maLBWindow->EnableRTL( IsRTLEnabled() ); + mpHScrollBar->EnableRTL( IsRTLEnabled() ); + mpVScrollBar->EnableRTL( IsRTLEnabled() ); + ImplResizeControls(); + } + + Control::StateChanged( nType ); +} + +bool ImplListBox::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, mpHScrollBar, mpVScrollBar ); + } + } + else if (rCEvt.GetCommand() == CommandEventId::Gesture) + { + bDone = HandleScrollCommand(rCEvt, mpHScrollBar, mpVScrollBar); + } + } + + return bDone || Window::EventNotify( rNEvt ); +} + +const Wallpaper& ImplListBox::GetDisplayBackground() const +{ + return maLBWindow->GetDisplayBackground(); +} + +bool ImplListBox::HandleWheelAsCursorTravel(const CommandEvent& rCEvt, Control& rControl) +{ + bool bDone = false; + if ( rCEvt.GetCommand() == CommandEventId::Wheel ) + { + const CommandWheelData* pData = rCEvt.GetWheelData(); + if( !pData->GetModifier() && ( pData->GetMode() == CommandWheelMode::SCROLL ) ) + { + if (!rControl.HasChildPathFocus()) + rControl.GrabFocus(); + sal_uInt16 nKey = ( pData->GetDelta() < 0 ) ? KEY_DOWN : KEY_UP; + KeyEvent aKeyEvent( 0, vcl::KeyCode( nKey ) ); + bDone = ProcessKeyInput( aKeyEvent ); + } + } + return bDone; +} + +void ImplListBox::SetMRUEntries( std::u16string_view rEntries, sal_Unicode cSep ) +{ + bool bChanges = GetEntryList().GetMRUCount() != 0; + + // Remove old MRU entries + for ( sal_Int32 n = GetEntryList().GetMRUCount();n; ) + maLBWindow->RemoveEntry( --n ); + + sal_Int32 nMRUCount = 0; + sal_Int32 nIndex = 0; + do + { + OUString aEntry( o3tl::getToken(rEntries, 0, cSep, nIndex ) ); + // Accept only existing entries + if ( GetEntryList().FindEntry( aEntry ) != LISTBOX_ENTRY_NOTFOUND ) + { + ImplEntryType* pNewEntry = new ImplEntryType( aEntry ); + maLBWindow->InsertEntry(nMRUCount++, pNewEntry, false); + bChanges = true; + } + } + while ( nIndex >= 0 ); + + if ( bChanges ) + { + maLBWindow->GetEntryList().SetMRUCount( nMRUCount ); + SetSeparatorPos( nMRUCount ? nMRUCount-1 : 0 ); + CompatStateChanged( StateChangedType::Data ); + } +} + +OUString ImplListBox::GetMRUEntries( sal_Unicode cSep ) const +{ + OUStringBuffer aEntries; + for ( sal_Int32 n = 0; n < GetEntryList().GetMRUCount(); n++ ) + { + aEntries.append(GetEntryList().GetEntryText( n )); + if( n < ( GetEntryList().GetMRUCount() - 1 ) ) + aEntries.append(cSep); + } + return aEntries.makeStringAndClear(); +} + +void ImplListBox::SetEdgeBlending(bool bNew) +{ + if(mbEdgeBlending != bNew) + { + mbEdgeBlending = bNew; + maLBWindow->SetEdgeBlending(GetEdgeBlending()); + } +} + +ImplWin::ImplWin( vcl::Window* pParent, WinBits nWinStyle ) : + Control ( pParent, nWinStyle ) +{ + if ( IsNativeControlSupported(ControlType::Listbox, ControlPart::Entire) + && ! IsNativeControlSupported(ControlType::Listbox, ControlPart::ButtonDown) ) + SetBackground(); + else + SetBackground( Wallpaper( GetSettings().GetStyleSettings().GetFieldColor() ) ); + + ImplGetWindowImpl()->mbUseNativeFocus = ImplGetSVData()->maNWFData.mbNoFocusRects; + + mbEdgeBlending = false; + mnItemPos = LISTBOX_ENTRY_NOTFOUND; +} + +void ImplWin::MouseButtonDown( const MouseEvent& ) +{ + if( IsEnabled() ) + { + maMBDownHdl.Call(this); + } +} + +void ImplWin::FillLayoutData() const +{ + mxLayoutData.emplace(); + ImplWin* pThis = const_cast<ImplWin*>(this); + pThis->ImplDraw(*pThis->GetOutDev(), true); +} + +bool ImplWin::PreNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE ) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if( pMouseEvt && (pMouseEvt->IsEnterWindow() || pMouseEvt->IsLeaveWindow()) ) + { + // trigger redraw as mouse over state has changed + if ( IsNativeControlSupported(ControlType::Listbox, ControlPart::Entire) + && ! IsNativeControlSupported(ControlType::Listbox, ControlPart::ButtonDown) ) + { + GetParent()->GetWindow( GetWindowType::Border )->Invalidate( InvalidateFlags::NoErase ); + } + } + } + + return Control::PreNotify(rNEvt); +} + +void ImplWin::ImplDraw(vcl::RenderContext& rRenderContext, bool bLayout) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + if (!bLayout) + { + bool bNativeOK = false; + bool bHasFocus = HasFocus(); + bool bIsEnabled = IsEnabled(); + + ControlState nState = ControlState::ENABLED; + if (rRenderContext.IsNativeControlSupported(ControlType::Listbox, ControlPart::Entire) + && rRenderContext.IsNativeControlSupported(ControlType::Listbox, ControlPart::HasBackgroundTexture) ) + { + // Repaint the (focused) area similarly to + // ImplSmallBorderWindowView::DrawWindow() in + // vcl/source/window/brdwin.cxx + vcl::Window *pWin = GetParent(); + + ImplControlValue aControlValue; + bIsEnabled &= pWin->IsEnabled(); + if ( !bIsEnabled ) + nState &= ~ControlState::ENABLED; + bHasFocus |= pWin->HasFocus(); + if ( bHasFocus ) + nState |= ControlState::FOCUSED; + + // The listbox is painted over the entire control including the + // border, but ImplWin does not contain the border => correction + // needed. + sal_Int32 nLeft, nTop, nRight, nBottom; + pWin->GetBorder( nLeft, nTop, nRight, nBottom ); + Point aPoint( -nLeft, -nTop ); + tools::Rectangle aCtrlRegion( aPoint - GetPosPixel(), pWin->GetSizePixel() ); + + bool bMouseOver = false; + vcl::Window *pChild = pWin->GetWindow( GetWindowType::FirstChild ); + while( pChild ) + { + bMouseOver = pChild->IsMouseOver(); + if (bMouseOver) + break; + pChild = pChild->GetWindow( GetWindowType::Next ); + } + if( bMouseOver ) + nState |= ControlState::ROLLOVER; + + // if parent has no border, then nobody has drawn the background + // since no border window exists. so draw it here. + WinBits nParentStyle = pWin->GetStyle(); + if( ! (nParentStyle & WB_BORDER) || (nParentStyle & WB_NOBORDER) ) + { + tools::Rectangle aParentRect( Point( 0, 0 ), pWin->GetSizePixel() ); + pWin->GetOutDev()->DrawNativeControl( ControlType::Listbox, ControlPart::Entire, aParentRect, + nState, aControlValue, OUString() ); + } + + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Listbox, ControlPart::Entire, aCtrlRegion, + nState, aControlValue, OUString()); + } + + if (bIsEnabled) + { + if (bHasFocus && !ImplGetSVData()->maNWFData.mbDDListBoxNoTextArea) + { + if ( !ImplGetSVData()->maNWFData.mbNoFocusRects ) + { + rRenderContext.SetFillColor( rStyleSettings.GetHighlightColor() ); + rRenderContext.SetTextColor( rStyleSettings.GetHighlightTextColor() ); + } + else + { + rRenderContext.SetLineColor(); + rRenderContext.SetFillColor(); + rRenderContext.SetTextColor( rStyleSettings.GetFieldTextColor() ); + } + rRenderContext.DrawRect( maFocusRect ); + } + else + { + Color aColor; + if (IsControlForeground()) + aColor = GetControlForeground(); + else if (ImplGetSVData()->maNWFData.mbDDListBoxNoTextArea) + { + if( bNativeOK && (nState & ControlState::ROLLOVER) ) + aColor = rStyleSettings.GetButtonRolloverTextColor(); + else + aColor = rStyleSettings.GetButtonTextColor(); + } + else + { + if( bNativeOK && (nState & ControlState::ROLLOVER) ) + aColor = rStyleSettings.GetFieldRolloverTextColor(); + else + aColor = rStyleSettings.GetFieldTextColor(); + } + rRenderContext.SetTextColor(aColor); + if (!bNativeOK) + rRenderContext.Erase(maFocusRect); + } + } + else // Disabled + { + rRenderContext.SetTextColor(rStyleSettings.GetDisableColor()); + if (!bNativeOK) + rRenderContext.Erase(maFocusRect); + } + } + + DrawEntry(rRenderContext, bLayout); +} + +void ImplWin::ApplySettings(vcl::RenderContext& rRenderContext) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + ApplyControlFont(rRenderContext, rStyleSettings.GetFieldFont()); + ApplyControlForeground(rRenderContext, rStyleSettings.GetFieldTextColor()); + + if (IsControlBackground()) + rRenderContext.SetBackground(GetControlBackground()); + else + rRenderContext.SetBackground(rStyleSettings.GetFieldColor()); +} + +void ImplWin::Paint( vcl::RenderContext& rRenderContext, const tools::Rectangle& ) +{ + ImplDraw(rRenderContext); +} + +void ImplWin::DrawEntry(vcl::RenderContext& rRenderContext, bool bLayout) +{ + tools::Long nBorder = 1; + Size aOutSz(GetOutputSizePixel()); + + bool bImage = !!maImage; + if (bImage && !bLayout) + { + DrawImageFlags nStyle = DrawImageFlags::NONE; + Size aImgSz = maImage.GetSizePixel(); + Point aPtImg( nBorder, ( ( aOutSz.Height() - aImgSz.Height() ) / 2 ) ); + const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); + + // check for HC mode + Image *pImage = &maImage; + + if ( !IsZoom() ) + { + rRenderContext.DrawImage( aPtImg, *pImage, nStyle ); + } + else + { + aImgSz.setWidth( CalcZoom( aImgSz.Width() ) ); + aImgSz.setHeight( CalcZoom( aImgSz.Height() ) ); + rRenderContext.DrawImage( aPtImg, aImgSz, *pImage, nStyle ); + } + + const sal_uInt16 nEdgeBlendingPercent(GetEdgeBlending() ? rStyleSettings.GetEdgeBlending() : 0); + + if(nEdgeBlendingPercent) + { + const Color& rTopLeft(rStyleSettings.GetEdgeBlendingTopLeftColor()); + const Color& rBottomRight(rStyleSettings.GetEdgeBlendingBottomRightColor()); + const sal_uInt8 nAlpha((nEdgeBlendingPercent * 255) / 100); + const BitmapEx aBlendFrame(createBlendFrame(aImgSz, nAlpha, rTopLeft, rBottomRight)); + + if(!aBlendFrame.IsEmpty()) + { + rRenderContext.DrawBitmapEx(aPtImg, aBlendFrame); + } + } + } + + if( !maString.isEmpty() ) + { + DrawTextFlags nTextStyle = DrawTextFlags::VCenter; + + if ( bImage && !bLayout ) + nTextStyle |= DrawTextFlags::Left; + else if ( GetStyle() & WB_CENTER ) + nTextStyle |= DrawTextFlags::Center; + else if ( GetStyle() & WB_RIGHT ) + nTextStyle |= DrawTextFlags::Right; + else + nTextStyle |= DrawTextFlags::Left; + + tools::Rectangle aTextRect( Point( nBorder, 0 ), Size( aOutSz.Width()-2*nBorder, aOutSz.Height() ) ); + + if ( bImage ) + { + aTextRect.AdjustLeft(maImage.GetSizePixel().Width() + IMG_TXT_DISTANCE ); + } + + std::vector< tools::Rectangle >* pVector = bLayout ? &mxLayoutData->m_aUnicodeBoundRects : nullptr; + OUString* pDisplayText = bLayout ? &mxLayoutData->m_aDisplayText : nullptr; + rRenderContext.DrawText( aTextRect, maString, nTextStyle, pVector, pDisplayText ); + } + + if( HasFocus() && !bLayout ) + ShowFocus( maFocusRect ); +} + +void ImplWin::Resize() +{ + Control::Resize(); + maFocusRect.SetSize( GetOutputSizePixel() ); + Invalidate(); +} + +void ImplWin::GetFocus() +{ + ShowFocus( maFocusRect ); + if( ImplGetSVData()->maNWFData.mbNoFocusRects && + IsNativeWidgetEnabled() && + IsNativeControlSupported( ControlType::Listbox, ControlPart::Entire ) ) + { + vcl::Window* pWin = GetParent()->GetWindow( GetWindowType::Border ); + if( ! pWin ) + pWin = GetParent(); + pWin->Invalidate(); + } + else + Invalidate(); + Control::GetFocus(); +} + +void ImplWin::LoseFocus() +{ + HideFocus(); + if( ImplGetSVData()->maNWFData.mbNoFocusRects && + IsNativeWidgetEnabled() && + IsNativeControlSupported( ControlType::Listbox, ControlPart::Entire ) ) + { + vcl::Window* pWin = GetParent()->GetWindow( GetWindowType::Border ); + if( ! pWin ) + pWin = GetParent(); + pWin->Invalidate(); + } + else + Invalidate(); + Control::LoseFocus(); +} + +void ImplWin::ShowFocus(const tools::Rectangle& rRect) +{ + if (IsNativeControlSupported(ControlType::Listbox, ControlPart::Focus)) + { + ImplControlValue aControlValue; + + vcl::Window *pWin = GetParent(); + tools::Rectangle aParentRect(Point(0, 0), pWin->GetSizePixel()); + pWin->GetOutDev()->DrawNativeControl(ControlType::Listbox, ControlPart::Focus, aParentRect, + ControlState::FOCUSED, aControlValue, OUString()); + } + Control::ShowFocus(rRect); +} + +ImplBtn::ImplBtn( vcl::Window* pParent, WinBits nWinStyle ) : + PushButton( pParent, nWinStyle ) +{ +} + +void ImplBtn::MouseButtonDown( const MouseEvent& ) +{ + if( IsEnabled() ) + maMBDownHdl.Call(this); +} + +ImplListBoxFloatingWindow::ImplListBoxFloatingWindow( vcl::Window* pParent ) : + FloatingWindow( pParent, WB_BORDER | WB_SYSTEMWINDOW | WB_NOSHADOW ) // no drop shadow for list boxes +{ + // for native widget rendering we must be able to detect this window type + SetType( WindowType::LISTBOXWINDOW ); + + mpImplLB = nullptr; + mnDDLineCount = 0; + mbAutoWidth = false; + + mnPopupModeStartSaveSelection = LISTBOX_ENTRY_NOTFOUND; + + vcl::Window * pBorderWindow = ImplGetBorderWindow(); + if( pBorderWindow ) + { + SetAccessibleRole(accessibility::AccessibleRole::PANEL); + pBorderWindow->SetAccessibleRole(accessibility::AccessibleRole::WINDOW); + } + else + { + SetAccessibleRole(accessibility::AccessibleRole::WINDOW); + } + +} + +ImplListBoxFloatingWindow::~ImplListBoxFloatingWindow() +{ + disposeOnce(); +} + +void ImplListBoxFloatingWindow::dispose() +{ + mpImplLB.clear(); + FloatingWindow::dispose(); +} + + +bool ImplListBoxFloatingWindow::PreNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if( !GetParent()->HasChildPathFocus( true ) ) + EndPopupMode(); + } + + return FloatingWindow::PreNotify( rNEvt ); +} + +void ImplListBoxFloatingWindow::setPosSizePixel( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, PosSizeFlags nFlags ) +{ + FloatingWindow::setPosSizePixel( nX, nY, nWidth, nHeight, nFlags ); + + // Fix #60890# ( MBA ): to be able to resize the Listbox even in its open state + // after a call to Resize(), we adjust its position if necessary + if ( IsReallyVisible() && ( nFlags & PosSizeFlags::Height ) ) + { + Point aPos = GetParent()->GetPosPixel(); + aPos = GetParent()->GetParent()->OutputToScreenPixel( aPos ); + + if ( nFlags & PosSizeFlags::X ) + aPos.setX( nX ); + + if ( nFlags & PosSizeFlags::Y ) + aPos.setY( nY ); + + sal_uInt16 nIndex; + SetPosPixel( ImplCalcPos( this, tools::Rectangle( aPos, GetParent()->GetSizePixel() ), FloatWinPopupFlags::Down, nIndex ) ); + } + +// if( !IsReallyVisible() ) + { + // The ImplListBox does not get a Resize() as not visible. + // But the windows must get a Resize(), so that the number of + // visible entries is correct for PgUp/PgDown. + // The number also cannot be calculated by List/Combobox, as for + // this the presence of the vertical Scrollbar has to be known. + mpImplLB->SetSizePixel( GetOutputSizePixel() ); + static_cast<vcl::Window*>(mpImplLB)->Resize(); + static_cast<vcl::Window*>(mpImplLB->GetMainWindow())->Resize(); + } +} + +void ImplListBoxFloatingWindow::Resize() +{ + mpImplLB->GetMainWindow()->ImplClearLayoutData(); + FloatingWindow::Resize(); +} + +Size ImplListBoxFloatingWindow::CalcFloatSize() const +{ + Size aFloatSz( maPrefSz ); + + sal_Int32 nLeft, nTop, nRight, nBottom; + GetBorder( nLeft, nTop, nRight, nBottom ); + + sal_Int32 nLines = mpImplLB->GetEntryList().GetEntryCount(); + if ( mnDDLineCount && ( nLines > mnDDLineCount ) ) + nLines = mnDDLineCount; + + Size aSz = mpImplLB->CalcSize( nLines ); + tools::Long nMaxHeight = aSz.Height() + nTop + nBottom; + + if ( mnDDLineCount ) + aFloatSz.setHeight( nMaxHeight ); + + if( mbAutoWidth ) + { + // AutoSize first only for width... + + aFloatSz.setWidth( aSz.Width() + nLeft + nRight ); + aFloatSz.AdjustWidth(nRight ); // adding some space looks better... + + if ( ( aFloatSz.Height() < nMaxHeight ) || ( mnDDLineCount && ( mnDDLineCount < mpImplLB->GetEntryList().GetEntryCount() ) ) ) + { + // then we also need the vertical Scrollbar + tools::Long nSBWidth = GetSettings().GetStyleSettings().GetScrollBarSize(); + aFloatSz.AdjustWidth(nSBWidth ); + } + + tools::Long nDesktopWidth = GetDesktopRectPixel().getWidth(); + if (aFloatSz.Width() > nDesktopWidth) + // Don't exceed the desktop width. + aFloatSz.setWidth( nDesktopWidth ); + } + + if ( aFloatSz.Height() > nMaxHeight ) + aFloatSz.setHeight( nMaxHeight ); + + // Minimal height, in case height is not set to Float height. + // The parent of FloatWin must be DropDown-Combo/Listbox. + Size aParentSz = GetParent()->GetSizePixel(); + if( (!mnDDLineCount || !nLines) && ( aFloatSz.Height() < aParentSz.Height() ) ) + aFloatSz.setHeight( aParentSz.Height() ); + + // do not get narrower than the parent... + if( aFloatSz.Width() < aParentSz.Width() ) + aFloatSz.setWidth( aParentSz.Width() ); + + // align height to entries... + tools::Long nInnerHeight = aFloatSz.Height() - nTop - nBottom; + tools::Long nEntryHeight = mpImplLB->GetEntryHeightWithMargin(); + if ( nInnerHeight % nEntryHeight ) + { + nInnerHeight /= nEntryHeight; + nInnerHeight++; + nInnerHeight *= nEntryHeight; + aFloatSz.setHeight( nInnerHeight + nTop + nBottom ); + } + + if (aFloatSz.Width() < aSz.Width()) + { + // The max width of list box entries exceeds the window width. + // Account for the scroll bar height. + tools::Long nSBWidth = GetSettings().GetStyleSettings().GetScrollBarSize(); + aFloatSz.AdjustHeight(nSBWidth ); + } + + return aFloatSz; +} + +void ImplListBoxFloatingWindow::StartFloat( bool bStartTracking ) +{ + if( IsInPopupMode() ) + return; + + Size aFloatSz = CalcFloatSize(); + + SetSizePixel( aFloatSz ); + mpImplLB->SetSizePixel( GetOutputSizePixel() ); + + sal_Int32 nPos = mpImplLB->GetEntryList().GetSelectedEntryPos( 0 ); + mnPopupModeStartSaveSelection = nPos; + + Size aSz = GetParent()->GetSizePixel(); + Point aPos = GetParent()->GetPosPixel(); + aPos = GetParent()->GetParent()->OutputToScreenPixel( aPos ); + // FIXME: this ugly hack is for Mac/Aqua + // should be replaced by a real mechanism to place the float rectangle + if( ImplGetSVData()->maNWFData.mbNoFocusRects && + GetParent()->IsNativeWidgetEnabled() ) + { + const sal_Int32 nLeft = 4, nTop = 4, nRight = 4, nBottom = 4; + aPos.AdjustX(nLeft ); + aPos.AdjustY(nTop ); + aSz.AdjustWidth( -(nLeft + nRight) ); + aSz.AdjustHeight( -(nTop + nBottom) ); + } + tools::Rectangle aRect( aPos, aSz ); + + // check if the control's parent is un-mirrored which is the case for form controls in a mirrored UI + // where the document is unmirrored + // because StartPopupMode() expects a rectangle in mirrored coordinates we have to re-mirror + vcl::Window *pGrandparent = GetParent()->GetParent(); + const OutputDevice *pGrandparentOutDev = pGrandparent->GetOutDev(); + + if( pGrandparent->GetOutDev()->ImplIsAntiparallel() ) + pGrandparentOutDev->ReMirror( aRect ); + + // mouse-button right: close the List-Box-Float-win and don't stop the handling fdo#84795 + StartPopupMode( aRect, FloatWinPopupFlags::Down | FloatWinPopupFlags::NoHorzPlacement | FloatWinPopupFlags::AllMouseButtonClose ); + + if( nPos != LISTBOX_ENTRY_NOTFOUND ) + mpImplLB->ShowProminentEntry( nPos ); + + if( bStartTracking ) + mpImplLB->GetMainWindow()->EnableMouseMoveSelect( true ); + + if ( mpImplLB->GetMainWindow()->IsGrabFocusAllowed() ) + mpImplLB->GetMainWindow()->GrabFocus(); + + mpImplLB->GetMainWindow()->ImplClearLayoutData(); + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/ivctrl.cxx b/vcl/source/control/ivctrl.cxx new file mode 100644 index 000000000..54b64373c --- /dev/null +++ b/vcl/source/control/ivctrl.cxx @@ -0,0 +1,634 @@ +/* -*- 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 <vcl/toolkit/ivctrl.hxx> +#include "imivctl.hxx" +#include <vcl/accessiblefactory.hxx> +#include <vcl/bitmapex.hxx> +#include <vcl/commandevent.hxx> +#include <vcl/mnemonic.hxx> +#include <vcl/settings.hxx> +#include <vcl/tabctrl.hxx> +#include <vcl/vclevent.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <vcl/uitest/logger.hxx> +#include <vcl/uitest/eventdescription.hxx> +#include <verticaltabctrl.hxx> + +using namespace ::com::sun::star::accessibility; + +namespace +{ +void collectUIInformation( const OUString& aID, const OUString& aPos) +{ + EventDescription aDescription; + aDescription.aID = aID; + aDescription.aParameters = {{ "POS" , aPos}}; + aDescription.aAction = "SELECT"; + aDescription.aKeyWord = "VerticalTab"; + UITestLogger::getInstance().logEvent(aDescription); +} +} + +/***************************************************************************** +| +| class : SvxIconChoiceCtrlEntry +| +\*****************************************************************************/ + +SvxIconChoiceCtrlEntry::SvxIconChoiceCtrlEntry( const OUString& rText, + const Image& rImage ) + : aImage(rImage) + , aText(rText) + , nPos(0) + , pblink(nullptr) + , pflink(nullptr) + , eTextMode(SvxIconChoiceCtrlTextMode::Short) + , nX(0) + , nY(0) + , nFlags(SvxIconViewFlags::NONE) +{ +} + +OUString SvxIconChoiceCtrlEntry::GetDisplayText() const +{ + return MnemonicGenerator::EraseAllMnemonicChars( aText ); +} + + +SvxIconChoiceCtrlColumnInfo::SvxIconChoiceCtrlColumnInfo( const SvxIconChoiceCtrlColumnInfo& rInfo ) +{ + nWidth = rInfo.nWidth; +} + +/***************************************************************************** +| +| class : SvtIconChoiceCtrl +| +\*****************************************************************************/ + +SvtIconChoiceCtrl::SvtIconChoiceCtrl( vcl::Window* pParent, WinBits nWinStyle ) : + + // WB_CLIPCHILDREN on, as ScrollBars lie on the window! + Control( pParent, nWinStyle | WB_CLIPCHILDREN ), + + _pImpl ( new SvxIconChoiceCtrl_Impl( this, nWinStyle ) ) +{ + GetOutDev()->SetLineColor(); + _pImpl->InitSettings(); + _pImpl->SetPositionMode( SvxIconChoiceCtrlPositionMode::AutoArrange ); +} + +void SvtIconChoiceCtrl::SetSelectionMode(SelectionMode eMode) +{ + _pImpl->SetSelectionMode(eMode); +} + +SvtIconChoiceCtrl::~SvtIconChoiceCtrl() +{ + disposeOnce(); +} + +void SvtIconChoiceCtrl::dispose() +{ + if (_pImpl) + { + _pImpl->CallEventListeners( VclEventId::ObjectDying, nullptr ); + _pImpl.reset(); + } + Control::dispose(); +} + +SvxIconChoiceCtrlEntry* SvtIconChoiceCtrl::InsertEntry( const OUString& rText, const Image& rImage ) +{ + SvxIconChoiceCtrlEntry* pEntry = new SvxIconChoiceCtrlEntry( rText, rImage); + + _pImpl->InsertEntry(std::unique_ptr<SvxIconChoiceCtrlEntry>(pEntry), _pImpl->GetEntryCount()); + + return pEntry; +} + +void SvtIconChoiceCtrl::RemoveEntry(sal_Int32 nIndex) +{ + _pImpl->RemoveEntry(nIndex); +} + +void SvtIconChoiceCtrl::DrawEntryImage( SvxIconChoiceCtrlEntry const * pEntry, const Point& rPos, OutputDevice& rDev ) +{ + rDev.DrawImage( rPos, pEntry->GetImage() ); +} + +OUString SvtIconChoiceCtrl::GetEntryText( SvxIconChoiceCtrlEntry const * pEntry ) +{ + return pEntry->GetText(); +} + +void SvtIconChoiceCtrl::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + _pImpl->Paint(rRenderContext, rRect); +} + +void SvtIconChoiceCtrl::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if( !_pImpl->MouseButtonDown( rMEvt ) ) + Control::MouseButtonDown( rMEvt ); +} + +void SvtIconChoiceCtrl::MouseButtonUp( const MouseEvent& rMEvt ) +{ + if( !_pImpl->MouseButtonUp( rMEvt ) ) + Control::MouseButtonUp( rMEvt ); +} + +void SvtIconChoiceCtrl::MouseMove( const MouseEvent& rMEvt ) +{ + if( !_pImpl->MouseMove( rMEvt ) ) + Control::MouseMove( rMEvt ); +} +void SvtIconChoiceCtrl::ArrangeIcons() +{ + if ( GetStyle() & WB_ALIGN_TOP ) + { + Size aFullSize; + tools::Rectangle aEntryRect; + + for ( sal_Int32 i = 0; i < GetEntryCount(); i++ ) + { + SvxIconChoiceCtrlEntry* pEntry = GetEntry ( i ); + aEntryRect = _pImpl->GetEntryBoundRect ( pEntry ); + + aFullSize.setWidth ( aFullSize.getWidth()+aEntryRect.GetWidth() ); + } + + _pImpl->Arrange ( false, aFullSize.getWidth(), 0 ); + } + else if ( GetStyle() & WB_ALIGN_LEFT ) + { + Size aFullSize; + tools::Rectangle aEntryRect; + + for ( sal_Int32 i = 0; i < GetEntryCount(); i++ ) + { + SvxIconChoiceCtrlEntry* pEntry = GetEntry ( i ); + aEntryRect = _pImpl->GetEntryBoundRect ( pEntry ); + + aFullSize.setHeight ( aFullSize.getHeight()+aEntryRect.GetHeight() ); + } + + _pImpl->Arrange ( false, 0, aFullSize.getHeight() ); + } + else + { + _pImpl->Arrange(false, 0, 0); + } + _pImpl->Arrange( false, 0, 1000 ); +} +void SvtIconChoiceCtrl::Resize() +{ + _pImpl->Resize(); + Control::Resize(); +} + +void SvtIconChoiceCtrl::GetFocus() +{ + _pImpl->GetFocus(); + Control::GetFocus(); + SvxIconChoiceCtrlEntry* pSelectedEntry = GetSelectedEntry(); + if ( pSelectedEntry ) + _pImpl->CallEventListeners( VclEventId::ListboxSelect, pSelectedEntry ); +} + +void SvtIconChoiceCtrl::LoseFocus() +{ + if (_pImpl) + _pImpl->LoseFocus(); + Control::LoseFocus(); +} + +void SvtIconChoiceCtrl::SetFont(const vcl::Font& rFont) +{ + if (rFont != GetFont()) + { + Control::SetFont(rFont); + _pImpl->FontModified(); + } +} + +void SvtIconChoiceCtrl::SetPointFont(const vcl::Font& rFont) +{ + if (rFont != GetPointFont(*GetOutDev())) //FIXME + { + Control::SetPointFont(*GetOutDev(), rFont); //FIXME + _pImpl->FontModified(); + } +} + +WinBits SvtIconChoiceCtrl::GetStyle() const +{ + return _pImpl->GetStyle(); +} + +void SvtIconChoiceCtrl::Command(const CommandEvent& rCEvt) +{ + _pImpl->Command( rCEvt ); + //pass at least alt press/release to parent impl + if (rCEvt.GetCommand() == CommandEventId::ModKeyChange) + Control::Command(rCEvt); +} + +#ifdef DBG_UTIL +void SvtIconChoiceCtrl::SetEntryTextMode( SvxIconChoiceCtrlTextMode eMode, SvxIconChoiceCtrlEntry* pEntry ) +{ + _pImpl->SetEntryTextMode( eMode, pEntry ); +} +#endif + +sal_Int32 SvtIconChoiceCtrl::GetEntryCount() const +{ + return _pImpl ? _pImpl->GetEntryCount() : 0; +} + +SvxIconChoiceCtrlEntry* SvtIconChoiceCtrl::GetEntry( sal_Int32 nPos ) const +{ + return _pImpl ? _pImpl->GetEntry( nPos ) : nullptr; +} + +SvxIconChoiceCtrlEntry* SvtIconChoiceCtrl::GetSelectedEntry() const +{ + return _pImpl ? _pImpl->GetFirstSelectedEntry() : nullptr; +} + +void SvtIconChoiceCtrl::ClickIcon() +{ + GetSelectedEntry(); + _aClickIconHdl.Call( this ); +} + +void SvtIconChoiceCtrl::KeyInput( const KeyEvent& rKEvt ) +{ + bool bKeyUsed = DoKeyInput( rKEvt ); + if ( !bKeyUsed ) + { + Control::KeyInput( rKEvt ); + } +} +bool SvtIconChoiceCtrl::DoKeyInput( const KeyEvent& rKEvt ) +{ + return _pImpl->KeyInput( rKEvt ); +} +sal_Int32 SvtIconChoiceCtrl::GetEntryListPos( SvxIconChoiceCtrlEntry const * pEntry ) const +{ + return _pImpl->GetEntryListPos( pEntry ); +} +SvxIconChoiceCtrlEntry* SvtIconChoiceCtrl::GetCursor( ) const +{ + return _pImpl->GetCurEntry( ); +} +void SvtIconChoiceCtrl::SetCursor( SvxIconChoiceCtrlEntry* pEntry ) +{ + _pImpl->SetCursor( pEntry ); +} + +void SvtIconChoiceCtrl::DataChanged( const DataChangedEvent& rDCEvt ) +{ + if ( ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + (rDCEvt.GetType() == DataChangedEventType::FONTS) ) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) ) + { + _pImpl->InitSettings(); + Invalidate(InvalidateFlags::NoChildren); + } + else + Control::DataChanged( rDCEvt ); +} + +void SvtIconChoiceCtrl::SetBackground( const Wallpaper& rPaper ) +{ + if( rPaper == GetBackground() ) + return; + + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + // if it is the default (empty) wallpaper + if (rPaper.IsEmpty()) + { + Control::SetBackground( rStyleSettings.GetFieldColor() ); + } + else + { + Wallpaper aBackground( rPaper ); + // HACK, as background might be transparent! + if( !aBackground.IsBitmap() ) + aBackground.SetStyle( WallpaperStyle::Tile ); + + WallpaperStyle eStyle = aBackground.GetStyle(); + Color aBack( aBackground.GetColor()); + if( aBack == COL_TRANSPARENT && + (!aBackground.IsBitmap() || + aBackground.GetBitmap().IsAlpha() || + (eStyle != WallpaperStyle::Tile && eStyle != WallpaperStyle::Scale)) ) + { + aBackground.SetColor( rStyleSettings.GetFieldColor() ); + } + if( aBackground.IsScrollable() ) + { + tools::Rectangle aRect; + aRect.SetSize( Size(32765, 32765) ); + aBackground.SetRect( aRect ); + } + else + { + tools::Rectangle aRect( _pImpl->GetOutputRect() ); + aBackground.SetRect( aRect ); + } + Control::SetBackground( aBackground ); + } + + // If text colors are attributed "hard," don't use automatism to select + // a readable text color. + vcl::Font aFont( GetFont() ); + aFont.SetColor( rStyleSettings.GetFieldTextColor() ); + SetFont( aFont ); + + Invalidate(InvalidateFlags::NoChildren); +} + +void SvtIconChoiceCtrl::RequestHelp( const HelpEvent& rHEvt ) +{ + if ( !_pImpl->RequestHelp( rHEvt ) ) + Control::RequestHelp( rHEvt ); +} + +tools::Rectangle SvtIconChoiceCtrl::GetBoundingBox( SvxIconChoiceCtrlEntry* pEntry ) const +{ + return _pImpl->GetEntryBoundRect( pEntry ); +} + +void SvtIconChoiceCtrl::FillLayoutData() const +{ + CreateLayoutData(); + const_cast<SvtIconChoiceCtrl*>(this)->Invalidate(); +} + +tools::Rectangle SvtIconChoiceCtrl::GetEntryCharacterBounds( const sal_Int32 _nEntryPos, const sal_Int32 _nCharacterIndex ) const +{ + tools::Rectangle aRect; + + Pair aEntryCharacterRange = GetLineStartEnd( _nEntryPos ); + if ( aEntryCharacterRange.A() + _nCharacterIndex < aEntryCharacterRange.B() ) + { + aRect = GetCharacterBounds( aEntryCharacterRange.A() + _nCharacterIndex ); + } + + return aRect; +} + +void SvtIconChoiceCtrl::SetNoSelection() +{ + _pImpl->SetNoSelection(); +} + +void SvtIconChoiceCtrl::CallImplEventListeners(VclEventId nEvent, void* pData) +{ + CallEventListeners(nEvent, pData); +} +css::uno::Reference< XAccessible > SvtIconChoiceCtrl::CreateAccessible() +{ + vcl::Window* pParent = GetAccessibleParentWindow(); + DBG_ASSERT( pParent, "SvTreeListBox::CreateAccessible - accessible parent not found" ); + + css::uno::Reference< XAccessible > xAccessible; + if ( pParent ) + { + css::uno::Reference< XAccessible > xAccParent = pParent->GetAccessible(); + if ( xAccParent.is() ) + { + css::uno::Reference< css::awt::XWindowPeer > xHoldAlive(GetComponentInterface()); + xAccessible = _pImpl->GetAccessibleFactory().createAccessibleIconChoiceCtrl( *this, xAccParent ); + } + } + return xAccessible; +} + +struct VerticalTabPageData +{ + OString sId; + SvxIconChoiceCtrlEntry* pEntry; + VclPtr<vcl::Window> xPage; ///< the TabPage itself +}; + +VerticalTabControl::VerticalTabControl(vcl::Window* pParent) + : VclHBox(pParent) + , m_xChooser(VclPtr<SvtIconChoiceCtrl>::Create(this, WB_3DLOOK | WB_ICON | WB_BORDER | + WB_NOCOLUMNHEADER | WB_HIGHLIGHTFRAME | + WB_NODRAGSELECTION | WB_TABSTOP | WB_CLIPCHILDREN | + WB_ALIGN_LEFT | WB_NOHSCROLL)) + , m_xBox(VclPtr<VclVBox>::Create(this)) +{ + SetStyle(GetStyle() | WB_DIALOGCONTROL); + SetType(WindowType::VERTICALTABCONTROL); + m_xChooser->SetSelectionMode(SelectionMode::Single); + m_xChooser->SetClickHdl(LINK(this, VerticalTabControl, ChosePageHdl_Impl)); + m_xChooser->set_width_request(110); + m_xChooser->set_height_request(400); + m_xChooser->SetSizePixel(Size(110, 400)); + m_xBox->set_vexpand(true); + m_xBox->set_hexpand(true); + m_xBox->set_expand(true); + m_xBox->Show(); + m_xChooser->Show(); +} + +VerticalTabControl::~VerticalTabControl() +{ + disposeOnce(); +} + +void VerticalTabControl::dispose() +{ + m_xChooser.disposeAndClear(); + m_xBox.disposeAndClear(); + VclHBox::dispose(); +} + +IMPL_LINK_NOARG(VerticalTabControl, ChosePageHdl_Impl, SvtIconChoiceCtrl*, void) +{ + SvxIconChoiceCtrlEntry *pEntry = m_xChooser->GetSelectedEntry(); + if (!pEntry) + pEntry = m_xChooser->GetCursor(); + + VerticalTabPageData* pData = GetPageData(pEntry); + + if (pData->sId != m_sCurrentPageId) + SetCurPageId(pData->sId); +} + +void VerticalTabControl::ActivatePage() +{ + m_aActivateHdl.Call( this ); +} + +bool VerticalTabControl::DeactivatePage() +{ + return !m_aDeactivateHdl.IsSet() || m_aDeactivateHdl.Call(this); +} + +VerticalTabPageData* VerticalTabControl::GetPageData(const SvxIconChoiceCtrlEntry* pEntry) const +{ + VerticalTabPageData* pRet = nullptr; + for (auto & pData : maPageList) + { + if (pData->pEntry == pEntry) + { + pRet = pData.get(); + break; + } + } + return pRet; +} + +VerticalTabPageData* VerticalTabControl::GetPageData(std::string_view rId) const +{ + VerticalTabPageData* pRet = nullptr; + for (auto & pData : maPageList) + { + if (pData->sId == rId) + { + pRet = pData.get(); + break; + } + } + return pRet; +} + +void VerticalTabControl::SetCurPageId(const OString& rId) +{ + OString sOldPageId = GetCurPageId(); + if (sOldPageId == rId) + return; + + VerticalTabPageData* pOldData = GetPageData(sOldPageId); + if (pOldData && pOldData->xPage) + { + if (!DeactivatePage()) + return; + pOldData->xPage->Hide(); + } + + m_sCurrentPageId = ""; + + VerticalTabPageData* pNewData = GetPageData(rId); + if (pNewData && pNewData->xPage) + { + m_sCurrentPageId = rId; + m_xChooser->SetCursor(pNewData->pEntry); + + ActivatePage(); + pNewData->xPage->Show(); + } + collectUIInformation(get_id(),OStringToOUString(m_sCurrentPageId,RTL_TEXTENCODING_UTF8)); +} + +const OString & VerticalTabControl::GetPageId(sal_uInt16 nIndex) const +{ + return maPageList[nIndex]->sId; +} + +void VerticalTabControl::InsertPage(const rtl::OString &rIdent, const rtl::OUString& rLabel, const Image& rImage, + const rtl::OUString& rTooltip, VclPtr<vcl::Window> xPage, int nPos) +{ + SvxIconChoiceCtrlEntry* pEntry = m_xChooser->InsertEntry(rLabel, rImage); + pEntry->SetQuickHelpText(rTooltip); + m_xChooser->ArrangeIcons(); + VerticalTabPageData* pNew; + if (nPos == -1) + { + maPageList.emplace_back(new VerticalTabPageData); + pNew = maPageList.back().get(); + } + else + { + maPageList.emplace(maPageList.begin() + nPos, new VerticalTabPageData); + pNew = maPageList[nPos].get(); + } + pNew->sId = rIdent; + pNew->pEntry = pEntry; + pNew->xPage = xPage; + Size aOrigPrefSize(m_xBox->get_preferred_size()); + Size aPagePrefSize(xPage->get_preferred_size()); + m_xBox->set_width_request(std::max(aOrigPrefSize.Width(), aPagePrefSize.Width())); + m_xBox->set_height_request(std::max(aOrigPrefSize.Height(), aPagePrefSize.Height())); + pNew->xPage->Hide(); +} + +void VerticalTabControl::RemovePage(std::string_view rPageId) +{ + for (auto it = maPageList.begin(), end = maPageList.end(); it != end; ++it) + { + VerticalTabPageData* pData = it->get(); + if (pData->sId == rPageId) + { + sal_Int32 nEntryListPos = m_xChooser->GetEntryListPos(pData->pEntry); + m_xChooser->RemoveEntry(nEntryListPos); + m_xChooser->ArrangeIcons(); + maPageList.erase(it); + break; + } + } +} + +sal_uInt16 VerticalTabControl::GetPagePos(std::string_view rPageId) const +{ + VerticalTabPageData* pData = GetPageData(rPageId); + if (!pData) + return TAB_PAGE_NOTFOUND; + return m_xChooser->GetEntryListPos(pData->pEntry); +} + +VclPtr<vcl::Window> VerticalTabControl::GetPage(std::string_view rPageId) const +{ + VerticalTabPageData* pData = GetPageData(rPageId); + if (!pData) + return nullptr; + return pData->xPage; +} + +OUString VerticalTabControl::GetPageText(std::string_view rPageId) const +{ + VerticalTabPageData* pData = GetPageData(rPageId); + if (!pData) + return OUString(); + return pData->pEntry->GetText(); +} + +void VerticalTabControl::SetPageText(std::string_view rPageId, const OUString& rText) +{ + VerticalTabPageData* pData = GetPageData(rPageId); + if (!pData) + return; + pData->pEntry->SetText(rText); +} + +FactoryFunction VerticalTabControl::GetUITestFactory() const +{ + return VerticalTabControlUIObject::create; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/listbox.cxx b/vcl/source/control/listbox.cxx new file mode 100644 index 000000000..c8c8b0c8d --- /dev/null +++ b/vcl/source/control/listbox.cxx @@ -0,0 +1,1427 @@ +/* -*- 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 <vcl/builder.hxx> +#include <vcl/commandevent.hxx> +#include <vcl/event.hxx> +#include <vcl/toolkit/lstbox.hxx> +#include <vcl/settings.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <sal/log.hxx> + +#include <svdata.hxx> +#include <listbox.hxx> +#include <dndeventdispatcher.hxx> +#include <comphelper/lok.hxx> + +#include <com/sun/star/datatransfer/dnd/XDropTarget.hpp> +#include <boost/property_tree/ptree.hpp> +#include <tools/json_writer.hxx> + +ListBox::ListBox(WindowType nType) + : Control(nType) + , mpImplLB(nullptr) +{ + ImplInitListBoxData(); +} + +ListBox::ListBox( vcl::Window* pParent, WinBits nStyle ) : Control( WindowType::LISTBOX ) +{ + ImplInitListBoxData(); + ImplInit( pParent, nStyle ); +} + +ListBox::~ListBox() +{ + disposeOnce(); +} + +void ListBox::dispose() +{ + CallEventListeners( VclEventId::ObjectDying ); + + mpImplLB.disposeAndClear(); + mpFloatWin.disposeAndClear(); + mpImplWin.disposeAndClear(); + mpBtn.disposeAndClear(); + + Control::dispose(); +} + +void ListBox::ImplInitListBoxData() +{ + mpFloatWin = nullptr; + mpImplWin = nullptr; + mpBtn = nullptr; + mnDDHeight = 0; + mnLineCount = 0; + m_nMaxWidthChars = -1; + mbDDAutoSize = true; +} + +void ListBox::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + nStyle = ImplInitStyle( nStyle ); + if ( !(nStyle & WB_NOBORDER) && ( nStyle & WB_DROPDOWN ) ) + nStyle |= WB_BORDER; + + Control::ImplInit( pParent, nStyle, nullptr ); + + css::uno::Reference< css::datatransfer::dnd::XDropTargetListener> xDrop = new DNDEventDispatcher(this); + + if( nStyle & WB_DROPDOWN ) + { + sal_Int32 nLeft, nTop, nRight, nBottom; + GetBorder( nLeft, nTop, nRight, nBottom ); + mnDDHeight = static_cast<sal_uInt16>(GetTextHeight() + nTop + nBottom + 4); + + if( IsNativeWidgetEnabled() && + IsNativeControlSupported( ControlType::Listbox, ControlPart::Entire ) ) + { + ImplControlValue aControlValue; + tools::Rectangle aCtrlRegion( Point( 0, 0 ), Size( 20, mnDDHeight ) ); + tools::Rectangle aBoundingRgn( aCtrlRegion ); + tools::Rectangle aContentRgn( aCtrlRegion ); + if( GetNativeControlRegion( ControlType::Listbox, ControlPart::Entire, aCtrlRegion, + ControlState::ENABLED, aControlValue, + aBoundingRgn, aContentRgn ) ) + { + sal_Int32 nHeight = aBoundingRgn.GetHeight(); + if( nHeight > mnDDHeight ) + mnDDHeight = static_cast<sal_uInt16>(nHeight); + } + } + + mpFloatWin = VclPtr<ImplListBoxFloatingWindow>::Create( this ); + if (!IsNativeControlSupported(ControlType::Pushbutton, ControlPart::Focus)) + mpFloatWin->RequestDoubleBuffering(true); + mpFloatWin->SetAutoWidth( true ); + mpFloatWin->SetPopupModeEndHdl( LINK( this, ListBox, ImplPopupModeEndHdl ) ); + mpFloatWin->GetDropTarget()->addDropTargetListener(xDrop); + + mpImplWin = VclPtr<ImplWin>::Create( this, (nStyle & (WB_LEFT|WB_RIGHT|WB_CENTER))|WB_NOBORDER ); + mpImplWin->SetMBDownHdl( LINK( this, ListBox, ImplClickBtnHdl ) ); + mpImplWin->Show(); + mpImplWin->GetDropTarget()->addDropTargetListener(xDrop); + mpImplWin->SetEdgeBlending(false); + + mpBtn = VclPtr<ImplBtn>::Create( this, WB_NOLIGHTBORDER | WB_RECTSTYLE ); + ImplInitDropDownButton( mpBtn ); + mpBtn->SetMBDownHdl( LINK( this, ListBox, ImplClickBtnHdl ) ); + mpBtn->Show(); + mpBtn->GetDropTarget()->addDropTargetListener(xDrop); + } + + vcl::Window* pLBParent = this; + if ( mpFloatWin ) + pLBParent = mpFloatWin; + mpImplLB = VclPtr<ImplListBox>::Create( pLBParent, nStyle&(~WB_BORDER) ); + mpImplLB->SetSelectHdl( LINK( this, ListBox, ImplSelectHdl ) ); + mpImplLB->SetScrollHdl( LINK( this, ListBox, ImplScrollHdl ) ); + mpImplLB->SetCancelHdl( LINK( this, ListBox, ImplCancelHdl ) ); + mpImplLB->SetDoubleClickHdl( LINK( this, ListBox, ImplDoubleClickHdl ) ); + mpImplLB->SetFocusHdl( LINK( this, ListBox, ImplFocusHdl ) ); + mpImplLB->SetListItemSelectHdl( LINK( this, ListBox, ImplListItemSelectHdl ) ); + mpImplLB->SetPosPixel( Point() ); + mpImplLB->SetEdgeBlending(false); + mpImplLB->Show(); + + mpImplLB->GetDropTarget()->addDropTargetListener(xDrop); + + if ( mpFloatWin ) + { + mpFloatWin->SetImplListBox( mpImplLB ); + mpImplLB->SetSelectionChangedHdl( LINK( this, ListBox, ImplSelectionChangedHdl ) ); + } + else + mpImplLB->GetMainWindow()->AllowGrabFocus( true ); + + SetCompoundControl( true ); +} + +WinBits ListBox::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + return nStyle; +} + +IMPL_LINK_NOARG(ListBox, ImplSelectHdl, LinkParamNone*, void) +{ + bool bPopup = IsInDropDown(); + if( IsDropDownBox() ) + { + if( !mpImplLB->IsTravelSelect() ) + { + mpFloatWin->EndPopupMode(); + mpImplWin->GrabFocus(); + } + + mpImplWin->SetItemPos( GetSelectedEntryPos() ); + mpImplWin->SetString( GetSelectedEntry() ); + if( mpImplLB->GetEntryList().HasImages() ) + { + Image aImage = mpImplLB->GetEntryList().GetEntryImage( GetSelectedEntryPos() ); + mpImplWin->SetImage( aImage ); + } + mpImplWin->Invalidate(); + } + + if ( ( !IsTravelSelect() || mpImplLB->IsSelectionChanged() ) || ( bPopup && !IsMultiSelectionEnabled() ) ) + Select(); +} + +IMPL_LINK( ListBox, ImplFocusHdl, sal_Int32, nPos, void ) +{ + CallEventListeners( VclEventId::ListboxFocus, reinterpret_cast<void*>(nPos) ); +} + +IMPL_LINK_NOARG( ListBox, ImplListItemSelectHdl, LinkParamNone*, void ) +{ + CallEventListeners( VclEventId::DropdownSelect ); +} + +IMPL_LINK_NOARG(ListBox, ImplScrollHdl, ImplListBox*, void) +{ + CallEventListeners( VclEventId::ListboxScrolled ); +} + +IMPL_LINK_NOARG(ListBox, ImplCancelHdl, LinkParamNone*, void) +{ + if( IsInDropDown() ) + mpFloatWin->EndPopupMode(); +} + +IMPL_LINK( ListBox, ImplSelectionChangedHdl, sal_Int32, nChanged, void ) +{ + if ( mpImplLB->IsTrackingSelect() ) + return; + + const ImplEntryList& rEntryList = mpImplLB->GetEntryList(); + if ( rEntryList.IsEntryPosSelected( nChanged ) ) + { + // FIXME? This should've been turned into an ImplPaintEntry some time ago... + if ( nChanged < rEntryList.GetMRUCount() ) + nChanged = rEntryList.FindEntry( rEntryList.GetEntryText( nChanged ) ); + mpImplWin->SetItemPos( nChanged ); + mpImplWin->SetString( rEntryList.GetEntryText( nChanged ) ); + if( rEntryList.HasImages() ) + { + Image aImage = rEntryList.GetEntryImage( nChanged ); + mpImplWin->SetImage( aImage ); + } + } + else + { + mpImplWin->SetItemPos( LISTBOX_ENTRY_NOTFOUND ); + mpImplWin->SetString( OUString() ); + Image aImage; + mpImplWin->SetImage( aImage ); + } + mpImplWin->Invalidate(); +} + +IMPL_LINK_NOARG(ListBox, ImplDoubleClickHdl, ImplListBoxWindow*, void) +{ + DoubleClick(); +} + +IMPL_LINK_NOARG(ListBox, ImplClickBtnHdl, void*, void) +{ + if( mpFloatWin->IsInPopupMode() ) + return; + + CallEventListeners( VclEventId::DropdownPreOpen ); + mpImplWin->GrabFocus(); + mpBtn->SetPressed( true ); + mpFloatWin->StartFloat( true ); + CallEventListeners( VclEventId::DropdownOpen ); + + ImplClearLayoutData(); + if( mpImplLB ) + mpImplLB->GetMainWindow()->ImplClearLayoutData(); + if( mpImplWin ) + mpImplWin->ImplClearLayoutData(); +} + +IMPL_LINK_NOARG(ListBox, ImplPopupModeEndHdl, FloatingWindow*, void) +{ + if( mpFloatWin->IsPopupModeCanceled() ) + { + if ( ( mpFloatWin->GetPopupModeStartSaveSelection() != LISTBOX_ENTRY_NOTFOUND ) + && !IsEntryPosSelected( mpFloatWin->GetPopupModeStartSaveSelection() ) ) + { + mpImplLB->SelectEntry( mpFloatWin->GetPopupModeStartSaveSelection(), true ); + bool bTravelSelect = mpImplLB->IsTravelSelect(); + mpImplLB->SetTravelSelect( true ); + + VclPtr<vcl::Window> xWindow = this; + Select(); + if ( xWindow->isDisposed() ) + return; + + mpImplLB->SetTravelSelect( bTravelSelect ); + } + } + + ImplClearLayoutData(); + if( mpImplLB ) + mpImplLB->GetMainWindow()->ImplClearLayoutData(); + if( mpImplWin ) + mpImplWin->ImplClearLayoutData(); + + mpBtn->SetPressed( false ); + CallEventListeners( VclEventId::DropdownClose ); +} + +void ListBox::ToggleDropDown() +{ + if( !IsDropDownBox() ) + return; + + if( mpFloatWin->IsInPopupMode() ) + mpFloatWin->EndPopupMode(); + else + { + CallEventListeners( VclEventId::DropdownPreOpen ); + mpImplWin->GrabFocus(); + mpBtn->SetPressed( true ); + mpFloatWin->StartFloat( true ); + CallEventListeners( VclEventId::DropdownOpen ); + } +} + +void ListBox::ApplySettings(vcl::RenderContext& rRenderContext) +{ + rRenderContext.SetBackground(); +} + +void ListBox::Draw( OutputDevice* pDev, const Point& rPos, SystemTextColorFlags nFlags ) +{ + mpImplLB->GetMainWindow()->ApplySettings(*pDev); + + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + vcl::Font aFont = mpImplLB->GetMainWindow()->GetDrawPixelFont( pDev ); + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + pDev->SetTextFillColor(); + + // Border/Background + pDev->SetLineColor(); + pDev->SetFillColor(); + bool bBorder = (GetStyle() & WB_BORDER); + bool bBackground = IsControlBackground(); + if ( bBorder || bBackground ) + { + tools::Rectangle aRect( aPos, aSize ); + if ( bBorder ) + { + ImplDrawFrame( pDev, aRect ); + } + if ( bBackground ) + { + pDev->SetFillColor( GetControlBackground() ); + pDev->DrawRect( aRect ); + } + } + + // Content + if ( nFlags & SystemTextColorFlags::Mono ) + { + pDev->SetTextColor( COL_BLACK ); + } + else + { + if ( !IsEnabled() ) + { + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + pDev->SetTextColor( rStyleSettings.GetDisableColor() ); + } + else + { + pDev->SetTextColor( GetTextColor() ); + } + } + + const tools::Long nOnePixel = GetDrawPixel( pDev, 1 ); + const tools::Long nOffX = 3*nOnePixel; + DrawTextFlags nTextStyle = DrawTextFlags::VCenter; + tools::Rectangle aTextRect( aPos, aSize ); + + if ( GetStyle() & WB_CENTER ) + nTextStyle |= DrawTextFlags::Center; + else if ( GetStyle() & WB_RIGHT ) + nTextStyle |= DrawTextFlags::Right; + else + nTextStyle |= DrawTextFlags::Left; + + aTextRect.AdjustLeft(nOffX ); + aTextRect.AdjustRight( -nOffX ); + + if ( IsDropDownBox() ) + { + OUString aText = GetSelectedEntry(); + tools::Long nTextHeight = pDev->GetTextHeight(); + tools::Long nTextWidth = pDev->GetTextWidth( aText ); + tools::Long nOffY = (aSize.Height()-nTextHeight) / 2; + + // Clipping? + if ( (nOffY < 0) || + ((nOffY+nTextHeight) > aSize.Height()) || + ((nOffX+nTextWidth) > aSize.Width()) ) + { + tools::Rectangle aClip( aPos, aSize ); + if ( nTextHeight > aSize.Height() ) + aClip.AdjustBottom(nTextHeight-aSize.Height()+1 ); // So that HP Printers don't optimize this away + pDev->IntersectClipRegion( aClip ); + } + + pDev->DrawText( aTextRect, aText, nTextStyle ); + } + else + { + tools::Long nTextHeight = pDev->GetTextHeight(); + sal_uInt16 nLines = ( nTextHeight > 0 ) ? static_cast<sal_uInt16>(aSize.Height() / nTextHeight) : 1; + tools::Rectangle aClip( aPos, aSize ); + + pDev->IntersectClipRegion( aClip ); + + if ( !nLines ) + nLines = 1; + + for ( sal_uInt16 n = 0; n < nLines; n++ ) + { + sal_Int32 nEntry = n+mpImplLB->GetTopEntry(); + bool bSelected = mpImplLB->GetEntryList().IsEntryPosSelected( nEntry ); + if ( bSelected ) + { + pDev->SetFillColor( COL_BLACK ); + pDev->DrawRect( tools::Rectangle( Point( aPos.X(), aPos.Y() + n*nTextHeight ), + Point( aPos.X() + aSize.Width(), aPos.Y() + (n+1)*nTextHeight + 2*nOnePixel ) ) ); + pDev->SetFillColor(); + pDev->SetTextColor( COL_WHITE ); + } + + aTextRect.SetTop( aPos.Y() + n*nTextHeight ); + aTextRect.SetBottom( aTextRect.Top() + nTextHeight ); + + pDev->DrawText( aTextRect, mpImplLB->GetEntryList().GetEntryText( nEntry ), nTextStyle ); + + if ( bSelected ) + pDev->SetTextColor( COL_BLACK ); + } + } + + pDev->Pop(); +} + +void ListBox::GetFocus() +{ + if ( mpImplLB ) + { + if( IsDropDownBox() ) + mpImplWin->GrabFocus(); + else + mpImplLB->GrabFocus(); + } + + Control::GetFocus(); +} + +void ListBox::LoseFocus() +{ + if( IsDropDownBox() ) + { + if (mpImplWin) + mpImplWin->HideFocus(); + } + else + { + if (mpImplLB) + mpImplLB->HideFocus(); + } + + Control::LoseFocus(); +} + +void ListBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( !((rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE))) ) + return; + + SetBackground(); // Due to a hack in Window::UpdateSettings the background must be reset + // otherwise it will overpaint NWF drawn listboxes + Resize(); + mpImplLB->Resize(); // Is not called by ListBox::Resize() if the ImplLB does not change + + if ( mpImplWin ) + { + mpImplWin->GetOutDev()->SetSettings( GetSettings() ); // If not yet set... + mpImplWin->ApplySettings(*mpImplWin->GetOutDev()); + + mpBtn->GetOutDev()->SetSettings( GetSettings() ); + ImplInitDropDownButton( mpBtn ); + } + + if ( IsDropDownBox() ) + Invalidate(); +} + +void ListBox::EnableAutoSize( bool bAuto ) +{ + mbDDAutoSize = bAuto; + if ( mpFloatWin ) + { + if ( bAuto && !mpFloatWin->GetDropDownLineCount() ) + { + // use GetListBoxMaximumLineCount here; before, was on fixed number of five + AdaptDropDownLineCountToMaximum(); + } + else if ( !bAuto ) + { + mpFloatWin->SetDropDownLineCount( 0 ); + } + } +} + +void ListBox::SetDropDownLineCount( sal_uInt16 nLines ) +{ + mnLineCount = nLines; + if ( mpFloatWin ) + mpFloatWin->SetDropDownLineCount( mnLineCount ); +} + +void ListBox::AdaptDropDownLineCountToMaximum() +{ + // Adapt to maximum allowed number. + // Limit for LOK as we can't render outside of the dialog canvas. + if (comphelper::LibreOfficeKit::isActive()) + SetDropDownLineCount(11); + else + SetDropDownLineCount(GetSettings().GetStyleSettings().GetListBoxMaximumLineCount()); +} + +sal_uInt16 ListBox::GetDropDownLineCount() const +{ + if ( mpFloatWin ) + return mpFloatWin->GetDropDownLineCount(); + return mnLineCount; +} + +void ListBox::setPosSizePixel( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, PosSizeFlags nFlags ) +{ + if( IsDropDownBox() && ( nFlags & PosSizeFlags::Size ) ) + { + Size aPrefSz = mpFloatWin->GetPrefSize(); + if ( ( nFlags & PosSizeFlags::Height ) && ( nHeight >= 2*mnDDHeight ) ) + aPrefSz.setHeight( nHeight-mnDDHeight ); + if ( nFlags & PosSizeFlags::Width ) + aPrefSz.setWidth( nWidth ); + mpFloatWin->SetPrefSize( aPrefSz ); + + if (IsAutoSizeEnabled()) + nHeight = mnDDHeight; + } + + Control::setPosSizePixel( nX, nY, nWidth, nHeight, nFlags ); +} + +void ListBox::Resize() +{ + Size aOutSz = GetOutputSizePixel(); + if( IsDropDownBox() ) + { + // Initialize the dropdown button size with the standard scrollbar width + tools::Long nSBWidth = GetSettings().GetStyleSettings().GetScrollBarSize(); + tools::Long nBottom = aOutSz.Height(); + + // Note: in case of no border, pBorder will actually be this + vcl::Window *pBorder = GetWindow( GetWindowType::Border ); + ImplControlValue aControlValue; + Point aPoint; + tools::Rectangle aContent, aBound; + + // Use the full extent of the control + tools::Rectangle aArea( aPoint, pBorder->GetOutputSizePixel() ); + + if ( GetNativeControlRegion( ControlType::Listbox, ControlPart::ButtonDown, + aArea, ControlState::NONE, aControlValue, aBound, aContent) ) + { + // Convert back from border space to local coordinates + aPoint = pBorder->ScreenToOutputPixel( OutputToScreenPixel( aPoint ) ); + aContent.Move( -aPoint.X(), -aPoint.Y() ); + + // Use the themes drop down size for the button + aOutSz.setWidth( aContent.Left() ); + mpBtn->setPosSizePixel( aContent.Left(), 0, aContent.GetWidth(), nBottom ); + + // Adjust the size of the edit field + if ( GetNativeControlRegion( ControlType::Listbox, ControlPart::SubEdit, + aArea, ControlState::NONE, aControlValue, aBound, aContent) ) + { + // Convert back from border space to local coordinates + aContent.Move( -aPoint.X(), -aPoint.Y() ); + + // Use the themes drop down size + if( ! (GetStyle() & WB_BORDER) && ImplGetSVData()->maNWFData.mbNoFocusRects ) + { + // No border but focus ring behavior -> we have a problem; the + // native rect relies on the border to draw the focus + // let's do the best we can and center vertically, so it doesn't look + // completely wrong. + Size aSz( GetOutputSizePixel() ); + tools::Long nDiff = aContent.Top() - (aSz.Height() - aContent.GetHeight())/2; + aContent.AdjustTop( -nDiff ); + aContent.AdjustBottom( -nDiff ); + } + mpImplWin->SetPosSizePixel( aContent.TopLeft(), aContent.GetSize() ); + } + else + mpImplWin->SetSizePixel( aOutSz ); + } + else + { + nSBWidth = CalcZoom( nSBWidth ); + mpImplWin->setPosSizePixel( 0, 0, aOutSz.Width() - nSBWidth, aOutSz.Height() ); + mpBtn->setPosSizePixel( aOutSz.Width() - nSBWidth, 0, nSBWidth, aOutSz.Height() ); + } + } + else + { + mpImplLB->SetSizePixel( aOutSz ); + } + + // Retain FloatingWindow size even when it's invisible, as we still process KEY_PGUP/DOWN ... + if ( mpFloatWin ) + mpFloatWin->SetSizePixel( mpFloatWin->CalcFloatSize() ); + + Control::Resize(); +} + +void ListBox::FillLayoutData() const +{ + mxLayoutData.emplace(); + const ImplListBoxWindow* rMainWin = mpImplLB->GetMainWindow(); + if( mpFloatWin ) + { + // Dropdown mode + AppendLayoutData( *mpImplWin ); + mpImplWin->SetLayoutDataParent( this ); + if( mpFloatWin->IsReallyVisible() ) + { + AppendLayoutData( *rMainWin ); + rMainWin->SetLayoutDataParent( this ); + } + } + else + { + AppendLayoutData( *rMainWin ); + rMainWin->SetLayoutDataParent( this ); + } +} + +tools::Long ListBox::GetIndexForPoint( const Point& rPoint, sal_Int32& rPos ) const +{ + if( !HasLayoutData() ) + FillLayoutData(); + + // Check whether rPoint fits at all + tools::Long nIndex = Control::GetIndexForPoint( rPoint ); + if( nIndex != -1 ) + { + // Point must be either in main list window + // or in impl window (dropdown case) + ImplListBoxWindow* rMain = mpImplLB->GetMainWindow(); + + // Convert coordinates to ImplListBoxWindow pixel coordinate space + Point aConvPoint = LogicToPixel( rPoint ); + aConvPoint = OutputToAbsoluteScreenPixel( aConvPoint ); + aConvPoint = rMain->AbsoluteScreenToOutputPixel( aConvPoint ); + aConvPoint = rMain->PixelToLogic( aConvPoint ); + + // Try to find entry + sal_Int32 nEntry = rMain->GetEntryPosForPoint( aConvPoint ); + if( nEntry == LISTBOX_ENTRY_NOTFOUND ) + { + // Not found, maybe dropdown case + if( mpImplWin && mpImplWin->IsReallyVisible() ) + { + // Convert to impl window pixel coordinates + aConvPoint = LogicToPixel( rPoint ); + aConvPoint = OutputToAbsoluteScreenPixel( aConvPoint ); + aConvPoint = mpImplWin->AbsoluteScreenToOutputPixel( aConvPoint ); + + // Check whether converted point is inside impl window + Size aImplWinSize = mpImplWin->GetOutputSizePixel(); + if( aConvPoint.X() >= 0 && aConvPoint.Y() >= 0 && aConvPoint.X() < aImplWinSize.Width() && aConvPoint.Y() < aImplWinSize.Height() ) + { + // Inside the impl window, the position is the current item pos + rPos = mpImplWin->GetItemPos(); + } + else + nIndex = -1; + } + else + nIndex = -1; + } + else + rPos = nEntry; + + SAL_WARN_IF( nIndex == -1, "vcl", "found index for point, but relative index failed" ); + } + + // Get line relative index + if( nIndex != -1 ) + nIndex = ToRelativeLineIndex( nIndex ); + + return nIndex; +} + +void ListBox::StateChanged( StateChangedType nType ) +{ + if( nType == StateChangedType::ReadOnly ) + { + if( mpImplWin ) + mpImplWin->Enable( !IsReadOnly() ); + if( mpBtn ) + mpBtn->Enable( !IsReadOnly() ); + } + else if( nType == StateChangedType::Enable ) + { + mpImplLB->Enable( IsEnabled() ); + if( mpImplWin ) + { + mpImplWin->Enable( IsEnabled() ); + if ( IsNativeControlSupported(ControlType::Listbox, ControlPart::Entire) + && ! IsNativeControlSupported(ControlType::Listbox, ControlPart::ButtonDown) ) + { + GetWindow( GetWindowType::Border )->Invalidate( InvalidateFlags::NoErase ); + } + else + mpImplWin->Invalidate(); + } + if( mpBtn ) + mpBtn->Enable( IsEnabled() ); + } + else if( nType == StateChangedType::UpdateMode ) + { + mpImplLB->SetUpdateMode( IsUpdateMode() ); + } + else if ( nType == StateChangedType::Zoom ) + { + mpImplLB->SetZoom( GetZoom() ); + if ( mpImplWin ) + { + mpImplWin->SetZoom( GetZoom() ); + mpImplWin->SetFont( mpImplLB->GetMainWindow()->GetFont() ); + mpImplWin->Invalidate(); + } + Resize(); + } + else if ( nType == StateChangedType::ControlFont ) + { + mpImplLB->SetControlFont( GetControlFont() ); + if ( mpImplWin ) + { + mpImplWin->SetControlFont( GetControlFont() ); + mpImplWin->SetFont( mpImplLB->GetMainWindow()->GetFont() ); + mpImplWin->Invalidate(); + } + Resize(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + mpImplLB->SetControlForeground( GetControlForeground() ); + if ( mpImplWin ) + { + mpImplWin->SetControlForeground( GetControlForeground() ); + mpImplWin->SetTextColor( GetControlForeground() ); + mpImplWin->SetFont( mpImplLB->GetMainWindow()->GetFont() ); + mpImplWin->Invalidate(); + } + } + else if ( nType == StateChangedType::ControlBackground ) + { + mpImplLB->SetControlBackground( GetControlBackground() ); + if ( mpImplWin ) + { + if ( mpImplWin->IsNativeControlSupported(ControlType::Listbox, ControlPart::Entire) ) + { + // Transparent background + mpImplWin->SetBackground(); + mpImplWin->SetControlBackground(); + } + else + { + mpImplWin->SetBackground( mpImplLB->GetMainWindow()->GetControlBackground() ); + mpImplWin->SetControlBackground( mpImplLB->GetMainWindow()->GetControlBackground() ); + } + mpImplWin->SetFont( mpImplLB->GetMainWindow()->GetFont() ); + mpImplWin->Invalidate(); + } + } + else if ( nType == StateChangedType::Style ) + { + SetStyle( ImplInitStyle( GetStyle() ) ); + mpImplLB->GetMainWindow()->EnableSort( ( GetStyle() & WB_SORT ) != 0 ); + bool bSimpleMode = ( GetStyle() & WB_SIMPLEMODE ) != 0; + mpImplLB->SetMultiSelectionSimpleMode( bSimpleMode ); + } + else if( nType == StateChangedType::Mirroring ) + { + if( mpBtn ) + { + mpBtn->EnableRTL( IsRTLEnabled() ); + ImplInitDropDownButton( mpBtn ); + } + mpImplLB->EnableRTL( IsRTLEnabled() ); + if( mpImplWin ) + mpImplWin->EnableRTL( IsRTLEnabled() ); + Resize(); + } + + Control::StateChanged( nType ); +} + +bool ListBox::PreNotify( NotifyEvent& rNEvt ) +{ + bool bDone = false; + if ( mpImplLB ) + { + if( ( rNEvt.GetType() == MouseNotifyEvent::KEYINPUT ) && ( rNEvt.GetWindow() == mpImplWin ) ) + { + KeyEvent aKeyEvt = *rNEvt.GetKeyEvent(); + switch( aKeyEvt.GetKeyCode().GetCode() ) + { + case KEY_DOWN: + { + if( mpFloatWin && !mpFloatWin->IsInPopupMode() && + aKeyEvt.GetKeyCode().IsMod2() ) + { + CallEventListeners( VclEventId::DropdownPreOpen ); + mpBtn->SetPressed( true ); + mpFloatWin->StartFloat( false ); + CallEventListeners( VclEventId::DropdownOpen ); + bDone = true; + } + else + { + bDone = mpImplLB->ProcessKeyInput( aKeyEvt ); + } + } + break; + case KEY_UP: + { + if( mpFloatWin && mpFloatWin->IsInPopupMode() && + aKeyEvt.GetKeyCode().IsMod2() ) + { + mpFloatWin->EndPopupMode(); + bDone = true; + } + else + { + bDone = mpImplLB->ProcessKeyInput( aKeyEvt ); + } + } + break; + case KEY_RETURN: + { + if( IsInDropDown() ) + { + mpImplLB->ProcessKeyInput( aKeyEvt ); + bDone = true; + } + } + break; + + default: + { + bDone = mpImplLB->ProcessKeyInput( aKeyEvt ); + } + } + } + else if ( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( IsInDropDown() && !HasChildPathFocus( true ) ) + mpFloatWin->EndPopupMode(); + } + else if ( (rNEvt.GetType() == MouseNotifyEvent::COMMAND) && + (rNEvt.GetCommandEvent()->GetCommand() == CommandEventId::Wheel) && + (rNEvt.GetWindow() == mpImplWin) ) + { + MouseWheelBehaviour nWheelBehavior( GetSettings().GetMouseSettings().GetWheelBehavior() ); + if ( ( nWheelBehavior == MouseWheelBehaviour::ALWAYS ) + || ( ( nWheelBehavior == MouseWheelBehaviour::FocusOnly ) + && HasChildPathFocus() + ) + ) + { + bDone = mpImplLB->HandleWheelAsCursorTravel(*rNEvt.GetCommandEvent(), *this); + } + else + { + bDone = false; // Don't consume this event, let the default handling take it (i.e. scroll the context) + } + } + } + + return bDone || Control::PreNotify( rNEvt ); +} + +void ListBox::Select() +{ + ImplCallEventListenersAndHandler( VclEventId::ListboxSelect, [this] () { maSelectHdl.Call(*this); } ); +} + +void ListBox::DoubleClick() +{ + ImplCallEventListenersAndHandler( VclEventId::ListboxDoubleClick, {} ); +} + +void ListBox::Clear() +{ + if (!mpImplLB) + return; + mpImplLB->Clear(); + if( IsDropDownBox() ) + { + mpImplWin->SetItemPos( LISTBOX_ENTRY_NOTFOUND ); + mpImplWin->SetString( OUString() ); + Image aImage; + mpImplWin->SetImage( aImage ); + mpImplWin->Invalidate(); + } + CallEventListeners( VclEventId::ListboxItemRemoved, reinterpret_cast<void*>(-1) ); +} + +void ListBox::SetNoSelection() +{ + mpImplLB->SetNoSelection(); + if( IsDropDownBox() ) + { + mpImplWin->SetItemPos( LISTBOX_ENTRY_NOTFOUND ); + mpImplWin->SetString( OUString() ); + Image aImage; + mpImplWin->SetImage( aImage ); + mpImplWin->Invalidate(); + } +} + +sal_Int32 ListBox::InsertEntry( const OUString& rStr, sal_Int32 nPos ) +{ + sal_Int32 nRealPos = mpImplLB->InsertEntry( nPos + mpImplLB->GetEntryList().GetMRUCount(), rStr ); + nRealPos = sal::static_int_cast<sal_Int32>(nRealPos - mpImplLB->GetEntryList().GetMRUCount()); + CallEventListeners( VclEventId::ListboxItemAdded, reinterpret_cast<void*>(nRealPos) ); + return nRealPos; +} + +sal_Int32 ListBox::InsertEntry( const OUString& rStr, const Image& rImage, sal_Int32 nPos ) +{ + sal_Int32 nRealPos = mpImplLB->InsertEntry( nPos + mpImplLB->GetEntryList().GetMRUCount(), rStr, rImage ); + nRealPos = sal::static_int_cast<sal_Int32>(nRealPos - mpImplLB->GetEntryList().GetMRUCount()); + CallEventListeners( VclEventId::ListboxItemAdded, reinterpret_cast<void*>(nRealPos) ); + return nRealPos; +} + +void ListBox::RemoveEntry( sal_Int32 nPos ) +{ + mpImplLB->RemoveEntry( nPos + mpImplLB->GetEntryList().GetMRUCount() ); + CallEventListeners( VclEventId::ListboxItemRemoved, reinterpret_cast<void*>(nPos) ); +} + +Image ListBox::GetEntryImage( sal_Int32 nPos ) const +{ + if ( mpImplLB && mpImplLB->GetEntryList().HasEntryImage( nPos ) ) + return mpImplLB->GetEntryList().GetEntryImage( nPos ); + return Image(); +} + +sal_Int32 ListBox::GetEntryPos( std::u16string_view rStr ) const +{ + if (!mpImplLB) + return LISTBOX_ENTRY_NOTFOUND; + sal_Int32 nPos = mpImplLB->GetEntryList().FindEntry( rStr ); + if ( nPos != LISTBOX_ENTRY_NOTFOUND ) + nPos = nPos - mpImplLB->GetEntryList().GetMRUCount(); + return nPos; +} + +OUString ListBox::GetEntry( sal_Int32 nPos ) const +{ + if (!mpImplLB) + return OUString(); + return mpImplLB->GetEntryList().GetEntryText( nPos + mpImplLB->GetEntryList().GetMRUCount() ); +} + +sal_Int32 ListBox::GetEntryCount() const +{ + if (!mpImplLB) + return 0; + return mpImplLB->GetEntryList().GetEntryCount() - mpImplLB->GetEntryList().GetMRUCount(); +} + +OUString ListBox::GetSelectedEntry(sal_Int32 nIndex) const +{ + return GetEntry( GetSelectedEntryPos( nIndex ) ); +} + +sal_Int32 ListBox::GetSelectedEntryCount() const +{ + if (!mpImplLB) + return 0; + return mpImplLB->GetEntryList().GetSelectedEntryCount(); +} + +sal_Int32 ListBox::GetSelectedEntryPos( sal_Int32 nIndex ) const +{ + if (!mpImplLB) + return LISTBOX_ENTRY_NOTFOUND; + + sal_Int32 nPos = mpImplLB->GetEntryList().GetSelectedEntryPos( nIndex ); + if ( nPos != LISTBOX_ENTRY_NOTFOUND ) + { + if ( nPos < mpImplLB->GetEntryList().GetMRUCount() ) + nPos = mpImplLB->GetEntryList().FindEntry( mpImplLB->GetEntryList().GetEntryText( nPos ) ); + nPos = nPos - mpImplLB->GetEntryList().GetMRUCount(); + } + return nPos; +} + +bool ListBox::IsEntryPosSelected( sal_Int32 nPos ) const +{ + return mpImplLB->GetEntryList().IsEntryPosSelected( nPos + mpImplLB->GetEntryList().GetMRUCount() ); +} + +void ListBox::SelectEntry( std::u16string_view rStr, bool bSelect ) +{ + SelectEntryPos( GetEntryPos( rStr ), bSelect ); +} + +void ListBox::SelectEntryPos( sal_Int32 nPos, bool bSelect ) +{ + if (!mpImplLB) + return; + + if ( 0 <= nPos && nPos < mpImplLB->GetEntryList().GetEntryCount() ) + { + sal_Int32 nCurrentPos = mpImplLB->GetCurrentPos(); + mpImplLB->SelectEntry( nPos + mpImplLB->GetEntryList().GetMRUCount(), bSelect ); + //Only when bSelect == true, send both Selection & Focus events + if (nCurrentPos != nPos && bSelect) + { + CallEventListeners( VclEventId::ListboxSelect, reinterpret_cast<void*>(nPos)); + if (HasFocus()) + CallEventListeners( VclEventId::ListboxFocus, reinterpret_cast<void*>(nPos)); + } + } +} + +void ListBox::SelectEntriesPos( const std::vector<sal_Int32>& rPositions, bool bSelect ) +{ + if (!mpImplLB) + return; + + bool bCallListeners = false; + + const sal_Int32 nCurrentPos = mpImplLB->GetCurrentPos(); + const auto nEntryCount = mpImplLB->GetEntryList().GetEntryCount(); + const auto nMRUCount = mpImplLB->GetEntryList().GetMRUCount(); + + for (auto nPos : rPositions) + { + if (0 <= nPos && nPos < nEntryCount) + { + mpImplLB->SelectEntry(nPos + nMRUCount, bSelect); + if (nCurrentPos != nPos && bSelect) + bCallListeners = true; + } + } + + //Only when bSelect == true, send both Selection & Focus events + if (bCallListeners) + { + CallEventListeners(VclEventId::ListboxSelect); + if (HasFocus()) + CallEventListeners(VclEventId::ListboxFocus); + } +} + +void ListBox::SetEntryData( sal_Int32 nPos, void* pNewData ) +{ + mpImplLB->SetEntryData( nPos + mpImplLB->GetEntryList().GetMRUCount(), pNewData ); +} + +void* ListBox::GetEntryData( sal_Int32 nPos ) const +{ + return mpImplLB->GetEntryList().GetEntryData( nPos + mpImplLB->GetEntryList().GetMRUCount() ); +} + +void ListBox::SetEntryFlags( sal_Int32 nPos, ListBoxEntryFlags nFlags ) +{ + mpImplLB->SetEntryFlags( nPos + mpImplLB->GetEntryList().GetMRUCount(), nFlags ); +} + +void ListBox::SetTopEntry( sal_Int32 nPos ) +{ + mpImplLB->SetTopEntry( nPos + mpImplLB->GetEntryList().GetMRUCount() ); +} + +sal_Int32 ListBox::GetTopEntry() const +{ + sal_Int32 nPos = GetEntryCount() ? mpImplLB->GetTopEntry() : LISTBOX_ENTRY_NOTFOUND; + if ( nPos < mpImplLB->GetEntryList().GetMRUCount() ) + nPos = 0; + return nPos; +} + +bool ListBox::IsTravelSelect() const +{ + return mpImplLB->IsTravelSelect(); +} + +bool ListBox::IsInDropDown() const +{ + // when the dropdown is dismissed, first mbInPopupMode is set to false, and on the next event iteration then + // mbPopupMode is set to false + return mpFloatWin && mpFloatWin->IsInPopupMode() && mpFloatWin->ImplIsInPrivatePopupMode(); +} + +tools::Rectangle ListBox::GetBoundingRectangle( sal_Int32 nItem ) const +{ + tools::Rectangle aRect = mpImplLB->GetMainWindow()->GetBoundingRectangle( nItem ); + tools::Rectangle aOffset = mpImplLB->GetMainWindow()->GetWindowExtentsRelative( static_cast<vcl::Window*>(const_cast<ListBox *>(this)) ); + aRect.Move( aOffset.Left(), aOffset.Top() ); + return aRect; +} + +void ListBox::EnableMultiSelection( bool bMulti ) +{ + mpImplLB->EnableMultiSelection( bMulti ); + + // WB_SIMPLEMODE: + // The MultiListBox behaves just like a normal ListBox + // MultiSelection is possible via corresponding additional keys + bool bSimpleMode = ( GetStyle() & WB_SIMPLEMODE ) != 0; + mpImplLB->SetMultiSelectionSimpleMode( bSimpleMode ); + + // In a MultiSelection, we can't see us travelling without focus + if ( mpFloatWin ) + mpImplLB->GetMainWindow()->AllowGrabFocus( bMulti ); +} + +bool ListBox::IsMultiSelectionEnabled() const +{ + return mpImplLB->IsMultiSelectionEnabled(); +} + +Size ListBox::CalcMinimumSize() const +{ + Size aSz; + + if (!mpImplLB) + return aSz; + + aSz = CalcSubEditSize(); + + bool bAddScrollWidth = false; + + if (IsDropDownBox()) + { + aSz.AdjustHeight(4 ); // add a space between entry and border + aSz.AdjustWidth(4 ); // add a little breathing space + bAddScrollWidth = true; + } + else + bAddScrollWidth = (GetStyle() & WB_VSCROLL) == WB_VSCROLL; + + if (bAddScrollWidth) + { + // Try native borders; scrollbar size may not be a good indicator + // See how large the edit area inside is to estimate what is needed for the dropdown + ImplControlValue aControlValue; + tools::Rectangle aContent, aBound; + Size aTestSize( 100, 20 ); + tools::Rectangle aArea( Point(), aTestSize ); + if( GetNativeControlRegion( ControlType::Listbox, ControlPart::SubEdit, aArea, ControlState::NONE, + aControlValue, aBound, aContent) ) + { + // use the themes drop down size + aSz.AdjustWidth(aTestSize.Width() - aContent.GetWidth() ); + } + else + aSz.AdjustWidth(GetSettings().GetStyleSettings().GetScrollBarSize() ); + } + + aSz = CalcWindowSize( aSz ); + + if (IsDropDownBox()) // Check minimum height of dropdown box + { + ImplControlValue aControlValue; + tools::Rectangle aRect( Point( 0, 0 ), aSz ); + tools::Rectangle aContent, aBound; + if( GetNativeControlRegion( ControlType::Listbox, ControlPart::Entire, aRect, ControlState::NONE, + aControlValue, aBound, aContent) ) + { + if( aBound.GetHeight() > aSz.Height() ) + aSz.setHeight( aBound.GetHeight() ); + } + } + + return aSz; +} + +Size ListBox::CalcSubEditSize() const +{ + Size aSz; + + if (!mpImplLB) + return aSz; + + if ( !IsDropDownBox() ) + aSz = mpImplLB->CalcSize (mnLineCount ? mnLineCount : mpImplLB->GetEntryList().GetEntryCount()); + else + { + aSz.setHeight( mpImplLB->GetEntryHeight() ); + // Size to maximum entry width + aSz.setWidth( mpImplLB->GetMaxEntryWidth() ); + + if (m_nMaxWidthChars != -1) + { + tools::Long nMaxWidth = m_nMaxWidthChars * approximate_char_width(); + aSz.setWidth( std::min(aSz.Width(), nMaxWidth) ); + } + + // Do not create ultrathin ListBoxes, it doesn't look good + if( aSz.Width() < GetSettings().GetStyleSettings().GetScrollBarSize() ) + aSz.setWidth( GetSettings().GetStyleSettings().GetScrollBarSize() ); + } + + return aSz; +} + +Size ListBox::GetOptimalSize() const +{ + return CalcMinimumSize(); +} + +Size ListBox::CalcAdjustedSize( const Size& rPrefSize ) const +{ + Size aSz = rPrefSize; + sal_Int32 nLeft, nTop, nRight, nBottom; + static_cast<vcl::Window*>(const_cast<ListBox *>(this))->GetBorder( nLeft, nTop, nRight, nBottom ); + aSz.AdjustHeight( -(nTop+nBottom) ); + if ( !IsDropDownBox() ) + { + tools::Long nEntryHeight = CalcBlockSize( 1, 1 ).Height(); + tools::Long nLines = aSz.Height() / nEntryHeight; + if ( nLines < 1 ) + nLines = 1; + aSz.setHeight( nLines * nEntryHeight ); + } + else + { + aSz.setHeight( mnDDHeight ); + } + aSz.AdjustHeight(nTop+nBottom ); + + aSz = CalcWindowSize( aSz ); + return aSz; +} + +Size ListBox::CalcBlockSize( sal_uInt16 nColumns, sal_uInt16 nLines ) const +{ + // ScrollBars are shown if needed + Size aMinSz = CalcMinimumSize(); + // aMinSz = ImplCalcOutSz( aMinSz ); + + Size aSz; + + // Height + if ( nLines ) + { + if ( !IsDropDownBox() ) + aSz.setHeight( mpImplLB->CalcSize( nLines ).Height() ); + else + aSz.setHeight( mnDDHeight ); + } + else + aSz.setHeight( aMinSz.Height() ); + + // Width + if ( nColumns ) + aSz.setWidth( nColumns * GetTextWidth( OUString('X') ) ); + else + aSz.setWidth( aMinSz.Width() ); + + if ( IsDropDownBox() ) + aSz.AdjustWidth(GetSettings().GetStyleSettings().GetScrollBarSize() ); + + if ( !IsDropDownBox() ) + { + if ( aSz.Width() < aMinSz.Width() ) + aSz.AdjustHeight(GetSettings().GetStyleSettings().GetScrollBarSize() ); + if ( aSz.Height() < aMinSz.Height() ) + aSz.AdjustWidth(GetSettings().GetStyleSettings().GetScrollBarSize() ); + } + + aSz = CalcWindowSize( aSz ); + return aSz; +} + +void ListBox::GetMaxVisColumnsAndLines( sal_uInt16& rnCols, sal_uInt16& rnLines ) const +{ + float nCharWidth = approximate_char_width(); + if ( !IsDropDownBox() ) + { + Size aOutSz = mpImplLB->GetMainWindow()->GetOutputSizePixel(); + rnCols = static_cast<sal_uInt16>(aOutSz.Width()/nCharWidth); + rnLines = static_cast<sal_uInt16>(aOutSz.Height()/mpImplLB->GetEntryHeightWithMargin()); + } + else + { + Size aOutSz = mpImplWin->GetOutputSizePixel(); + rnCols = static_cast<sal_uInt16>(aOutSz.Width()/nCharWidth); + rnLines = 1; + } +} + +void ListBox::SetReadOnly( bool bReadOnly ) +{ + if ( mpImplLB->IsReadOnly() != bReadOnly ) + { + mpImplLB->SetReadOnly( bReadOnly ); + CompatStateChanged( StateChangedType::ReadOnly ); + } +} + +bool ListBox::IsReadOnly() const +{ + return mpImplLB->IsReadOnly(); +} + +void ListBox::SetSeparatorPos( sal_Int32 n ) +{ + mpImplLB->SetSeparatorPos( n ); +} + +sal_Int32 ListBox::GetSeparatorPos() const +{ + return mpImplLB->GetSeparatorPos(); +} + +void ListBox::AddSeparator( sal_Int32 n ) +{ + mpImplLB->AddSeparator( n ); +} + +sal_uInt16 ListBox::GetDisplayLineCount() const +{ + return mpImplLB->GetDisplayLineCount(); +} + +tools::Rectangle ListBox::GetDropDownPosSizePixel() const +{ + return mpFloatWin ? mpFloatWin->GetWindowExtentsRelative(this) : tools::Rectangle(); +} + +const Wallpaper& ListBox::GetDisplayBackground() const +{ + // !!! Recursion does not occur because the ImplListBox is initialized by default + // to a non-transparent color in Window::ImplInitData + return mpImplLB->GetDisplayBackground(); +} + +void ListBox::setMaxWidthChars(sal_Int32 nWidth) +{ + if (nWidth != m_nMaxWidthChars) + { + m_nMaxWidthChars = nWidth; + queue_resize(); + } +} + +bool ListBox::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "active") + SelectEntryPos(rValue.toInt32()); + else if (rKey == "max-width-chars") + setMaxWidthChars(rValue.toInt32()); + else if (rKey == "can-focus") + { + // as far as I can see in Gtk, setting a ComboBox as can.focus means + // the focus gets stuck in it, so try here to behave like gtk does + // with the settings that work, i.e. can.focus of false doesn't + // set the hard WB_NOTABSTOP + WinBits nBits = GetStyle(); + nBits &= ~(WB_TABSTOP|WB_NOTABSTOP); + if (toBool(rValue)) + nBits |= WB_TABSTOP; + SetStyle(nBits); + } + else + return Control::set_property(rKey, rValue); + return true; +} + +FactoryFunction ListBox::GetUITestFactory() const +{ + return ListBoxUIObject::create; +} + +void ListBox::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Control::DumpAsPropertyTree(rJsonWriter); + + { + auto entriesNode = rJsonWriter.startArray("entries"); + for (int i = 0; i < GetEntryCount(); ++i) + { + rJsonWriter.putSimpleValue(GetEntry(i)); + } + } + + rJsonWriter.put("selectedCount", GetSelectedEntryCount()); + + { + auto entriesNode = rJsonWriter.startArray("selectedEntries"); + for (int i = 0; i < GetSelectedEntryCount(); ++i) + { + rJsonWriter.putSimpleValue(OUString::number(GetSelectedEntryPos(i))); + } + } +} + +MultiListBox::MultiListBox( vcl::Window* pParent, WinBits nStyle ) : + ListBox( WindowType::MULTILISTBOX ) +{ + ImplInit( pParent, nStyle ); + EnableMultiSelection( true ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/longcurr.cxx b/vcl/source/control/longcurr.cxx new file mode 100644 index 000000000..c2c6e4134 --- /dev/null +++ b/vcl/source/control/longcurr.cxx @@ -0,0 +1,429 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <string_view> + +#include <comphelper/string.hxx> +#include <tools/bigint.hxx> +#include <sal/log.hxx> + +#include <vcl/event.hxx> +#include <vcl/svapp.hxx> +#include <vcl/toolkit/longcurr.hxx> +#include <vcl/weldutils.hxx> + +#include <unotools/localedatawrapper.hxx> + +using namespace ::comphelper; + +namespace +{ + +BigInt ImplPower10( sal_uInt16 n ) +{ + sal_uInt16 i; + BigInt nValue = 1; + + for ( i=0; i < n; i++ ) + nValue *= 10; + + return nValue; +} + +OUString ImplGetCurr( const LocaleDataWrapper& rLocaleDataWrapper, const BigInt &rNumber, sal_uInt16 nDigits, std::u16string_view rCurrSymbol, bool bShowThousandSep ) +{ + SAL_WARN_IF( nDigits >= 10, "vcl", "LongCurrency may only have 9 decimal places" ); + + if ( rNumber.IsZero() || static_cast<tools::Long>(rNumber) ) + return rLocaleDataWrapper.getCurr( static_cast<tools::Long>(rNumber), nDigits, rCurrSymbol, bShowThousandSep ); + + BigInt aTmp( ImplPower10( nDigits ) ); + BigInt aInteger( rNumber ); + aInteger.Abs(); + aInteger /= aTmp; + BigInt aFraction( rNumber ); + aFraction.Abs(); + aFraction %= aTmp; + if ( !aInteger.IsZero() ) + { + aFraction += aTmp; + aTmp = 1000000000; + } + if ( rNumber.IsNeg() ) + aFraction *= -1; + + OUStringBuffer aTemplate(rLocaleDataWrapper.getCurr( static_cast<tools::Long>(aFraction), nDigits, rCurrSymbol, bShowThousandSep )); + while( !aInteger.IsZero() ) + { + aFraction = aInteger; + aFraction %= aTmp; + aInteger /= aTmp; + if( !aInteger.IsZero() ) + aFraction += aTmp; + + OUString aFractionStr = rLocaleDataWrapper.getNum( static_cast<tools::Long>(aFraction), 0 ); + + sal_Int32 nSPos = aTemplate.indexOf( '1' ); + if (nSPos == -1) + break; + if ( aFractionStr.getLength() == 1 ) + aTemplate[ nSPos ] = aFractionStr[0]; + else + { + aTemplate.remove( nSPos, 1 ); + aTemplate.insert( nSPos, aFractionStr ); + } + } + + return aTemplate.makeStringAndClear(); +} + +bool ImplCurrencyGetValue( const OUString& rStr, BigInt& rValue, + sal_uInt16 nDecDigits, const LocaleDataWrapper& rLocaleDataWrapper ) +{ + OUString aStr = rStr; + OUStringBuffer aStr1; + OUStringBuffer aStr2; + sal_Int32 nDecPos; + bool bNegative = false; + + // On empty string + if ( rStr.isEmpty() ) + return false; + + // Trim leading and trailing spaces + aStr = string::strip(aStr, ' '); + + // Find decimal sign's position + nDecPos = aStr.indexOf( rLocaleDataWrapper.getNumDecimalSep() ); + if (nDecPos < 0 && !rLocaleDataWrapper.getNumDecimalSepAlt().isEmpty()) + nDecPos = aStr.indexOf( rLocaleDataWrapper.getNumDecimalSepAlt() ); + + if ( nDecPos != -1 ) + { + aStr1 = aStr.subView( 0, nDecPos ); + aStr2.append(aStr.subView(nDecPos+1)); + } + else + aStr1 = aStr; + + // Negative? + if ( (aStr[ 0 ] == '(') && (aStr[ aStr.getLength()-1 ] == ')') ) + bNegative = true; + if ( !bNegative ) + { + for (sal_Int32 i=0; i < aStr.getLength(); i++ ) + { + if ( (aStr[ i ] >= '0') && (aStr[ i ] <= '9') ) + break; + else if ( aStr[ i ] == '-' ) + { + bNegative = true; + break; + } + } + } + if ( !bNegative && !aStr.isEmpty() ) + { + sal_uInt16 nFormat = rLocaleDataWrapper.getCurrNegativeFormat(); + if ( (nFormat == 3) || (nFormat == 6) || + (nFormat == 7) || (nFormat == 10) ) + { + for (sal_Int32 i = aStr.getLength()-1; i > 0; i++ ) + { + if ( (aStr[ i ] >= '0') && (aStr[ i ] <= '9') ) + break; + else if ( aStr[ i ] == '-' ) + { + bNegative = true; + break; + } + } + } + } + + // delete unwanted characters + for (sal_Int32 i=0; i < aStr1.getLength(); ) + { + if ( (aStr1[ i ] >= '0') && (aStr1[ i ] <= '9') ) + i++; + else + aStr1.remove( i, 1 ); + } + for (sal_Int32 i=0; i < aStr2.getLength(); ) + { + if ((aStr2[i] >= '0') && (aStr2[i] <= '9')) + ++i; + else + aStr2.remove(i, 1); + } + + if ( aStr1.isEmpty() && aStr2.isEmpty()) + return false; + + if ( aStr1.isEmpty() ) + aStr1 = "0"; + if ( bNegative ) + aStr1.insert( 0, '-'); + + // Cut down decimal part and round while doing so + bool bRound = false; + if (aStr2.getLength() > nDecDigits) + { + if (aStr2[nDecDigits] >= '5') + bRound = true; + string::truncateToLength(aStr2, nDecDigits); + } + string::padToLength(aStr2, nDecDigits, '0'); + + aStr1.append(aStr2); + aStr = aStr1.makeStringAndClear(); + + // check range + BigInt nValue( aStr ); + if ( bRound ) + { + if ( !bNegative ) + nValue+=1; + else + nValue-=1; + } + + rValue = nValue; + + return true; +} + +} // namespace + +static bool ImplLongCurrencyGetValue( const OUString& rStr, BigInt& rValue, + sal_uInt16 nDecDigits, const LocaleDataWrapper& rLocaleDataWrapper ) +{ + return ImplCurrencyGetValue( rStr, rValue, nDecDigits, rLocaleDataWrapper ); +} + +namespace weld +{ + IMPL_LINK_NOARG(LongCurrencyFormatter, FormatOutputHdl, LinkParamNone*, bool) + { + const LocaleDataWrapper& rLocaleDataWrapper = Application::GetSettings().GetLocaleDataWrapper(); + const OUString& rCurrencySymbol = !m_aCurrencySymbol.isEmpty() ? m_aCurrencySymbol : rLocaleDataWrapper.getCurrSymbol(); + double fValue = GetValue() * weld::SpinButton::Power10(GetDecimalDigits()); + OUString aText = ImplGetCurr(rLocaleDataWrapper, fValue, GetDecimalDigits(), rCurrencySymbol, m_bThousandSep); + ImplSetTextImpl(aText, nullptr); + return true; + } + + IMPL_LINK(LongCurrencyFormatter, ParseInputHdl, sal_Int64*, result, TriState) + { + const LocaleDataWrapper& rLocaleDataWrapper = Application::GetSettings().GetLocaleDataWrapper(); + + BigInt value; + bool bRet = ImplLongCurrencyGetValue(GetEntryText(), value, GetDecimalDigits(), rLocaleDataWrapper); + + if (bRet) + *result = double(value); + + return bRet ? TRISTATE_TRUE : TRISTATE_FALSE; + } +} + +bool ImplLongCurrencyReformat( const OUString& rStr, BigInt const & nMin, BigInt const & nMax, + sal_uInt16 nDecDigits, + const LocaleDataWrapper& rLocaleDataWrapper, OUString& rOutStr, + LongCurrencyFormatter const & rFormatter ) +{ + BigInt nValue; + if ( !ImplCurrencyGetValue( rStr, nValue, nDecDigits, rLocaleDataWrapper ) ) + return true; + else + { + BigInt nTempVal = nValue; + if ( nTempVal > nMax ) + nTempVal = nMax; + else if ( nTempVal < nMin ) + nTempVal = nMin; + + rOutStr = ImplGetCurr( rLocaleDataWrapper, nTempVal, nDecDigits, rFormatter.GetCurrencySymbol(), /*IsUseThousandSep*/true ); + return true; + } +} + +void LongCurrencyFormatter::ImpInit() +{ + mnLastValue = 0; + mnMin = 0; + mnMax = 0x7FFFFFFF; + mnMax *= 0x7FFFFFFF; + mnDecimalDigits = 0; + SetDecimalDigits( 0 ); +} + +LongCurrencyFormatter::LongCurrencyFormatter(Edit* pEdit) + : FormatterBase(pEdit) +{ + ImpInit(); +} + +LongCurrencyFormatter::~LongCurrencyFormatter() +{ +} + +OUString const & LongCurrencyFormatter::GetCurrencySymbol() const +{ + return GetLocaleDataWrapper().getCurrSymbol(); +} + +void LongCurrencyFormatter::SetValue(const BigInt& rNewValue) +{ + SetUserValue(rNewValue); + SetEmptyFieldValueData( false ); +} + +void LongCurrencyFormatter::SetUserValue( BigInt nNewValue ) +{ + if ( nNewValue > mnMax ) + nNewValue = mnMax; + else if ( nNewValue < mnMin ) + nNewValue = mnMin; + mnLastValue = nNewValue; + + if ( !GetField() ) + return; + + OUString aStr = ImplGetCurr( GetLocaleDataWrapper(), nNewValue, GetDecimalDigits(), GetCurrencySymbol(), /*UseThousandSep*/true ); + if ( GetField()->HasFocus() ) + { + Selection aSelection = GetField()->GetSelection(); + GetField()->SetText( aStr ); + GetField()->SetSelection( aSelection ); + } + else + GetField()->SetText( aStr ); + MarkToBeReformatted( false ); +} + +BigInt LongCurrencyFormatter::GetValue() const +{ + if ( !GetField() ) + return 0; + + BigInt nTempValue; + if ( ImplLongCurrencyGetValue( GetField()->GetText(), nTempValue, GetDecimalDigits(), GetLocaleDataWrapper() ) ) + { + if ( nTempValue > mnMax ) + nTempValue = mnMax; + else if ( nTempValue < mnMin ) + nTempValue = mnMin; + return nTempValue; + } + else + return mnLastValue; +} + +void LongCurrencyFormatter::Reformat() +{ + if ( !GetField() ) + return; + + if ( GetField()->GetText().isEmpty() && ImplGetEmptyFieldValue() ) + return; + + OUString aStr; + bool bOK = ImplLongCurrencyReformat( GetField()->GetText(), mnMin, mnMax, + GetDecimalDigits(), GetLocaleDataWrapper(), aStr, *this ); + if ( !bOK ) + return; + + if ( !aStr.isEmpty() ) + { + GetField()->SetText( aStr ); + MarkToBeReformatted( false ); + ImplLongCurrencyGetValue( aStr, mnLastValue, GetDecimalDigits(), GetLocaleDataWrapper() ); + } + else + SetValue( mnLastValue ); +} + +void LongCurrencyFormatter::ReformatAll() +{ + Reformat(); +} + +void LongCurrencyFormatter::SetDecimalDigits( sal_uInt16 nDigits ) +{ + if ( nDigits > 9 ) + nDigits = 9; + + mnDecimalDigits = nDigits; + ReformatAll(); +} + + + +LongCurrencyBox::LongCurrencyBox(vcl::Window* pParent, WinBits nWinStyle) + : ComboBox(pParent, nWinStyle) + , LongCurrencyFormatter(this) +{ + Reformat(); +} + +bool LongCurrencyBox::EventNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::GETFOCUS ) + { + MarkToBeReformatted( false ); + } + else if( rNEvt.GetType() == MouseNotifyEvent::LOSEFOCUS ) + { + if ( MustBeReformatted() ) + { + Reformat(); + ComboBox::Modify(); + } + } + return ComboBox::EventNotify( rNEvt ); +} + +void LongCurrencyBox::Modify() +{ + MarkToBeReformatted( true ); + ComboBox::Modify(); +} + +void LongCurrencyBox::ReformatAll() +{ + OUString aStr; + SetUpdateMode( false ); + const sal_Int32 nEntryCount = GetEntryCount(); + for ( sal_Int32 i=0; i < nEntryCount; ++i ) + { + ImplLongCurrencyReformat( GetEntry( i ), mnMin, mnMax, + GetDecimalDigits(), GetLocaleDataWrapper(), + aStr, *this ); + RemoveEntryAt(i); + InsertEntry( aStr, i ); + } + LongCurrencyFormatter::Reformat(); + SetUpdateMode( true ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/managedmenubutton.cxx b/vcl/source/control/managedmenubutton.cxx new file mode 100644 index 000000000..bf3065e71 --- /dev/null +++ b/vcl/source/control/managedmenubutton.cxx @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <comphelper/processfactory.hxx> +#include <comphelper/propertyvalue.hxx> + +#include <managedmenubutton.hxx> +#include <vcl/menu.hxx> + +#include <com/sun/star/frame/ModuleManager.hpp> +#include <com/sun/star/frame/theDesktop.hpp> +#include <com/sun/star/frame/thePopupMenuControllerFactory.hpp> + +ManagedMenuButton::ManagedMenuButton(vcl::Window* pParent, WinBits nStyle) + : MenuButton(pParent, nStyle) +{ + SetImageAlign(ImageAlign::Left); +} + +ManagedMenuButton::~ManagedMenuButton() +{ + disposeOnce(); +} + +void ManagedMenuButton::dispose() +{ + css::uno::Reference<css::lang::XComponent> xComponent(m_xPopupController, css::uno::UNO_QUERY); + if (xComponent.is()) + xComponent->dispose(); + + m_xPopupMenu.clear(); + m_xPopupController.clear(); + MenuButton::dispose(); +} + +void ManagedMenuButton::Activate() +{ + if (!GetPopupMenu()) + SetPopupMenu(VclPtr<PopupMenu>::Create()); + + MenuButton::Activate(); + + if (m_xPopupController.is()) + { + m_xPopupController->updatePopupMenu(); + return; + } + + if (!m_xPopupMenu.is()) + m_xPopupMenu = GetPopupMenu()->CreateMenuInterface(); + + // FIXME: get the frame from the parent VclBuilder. + css::uno::Reference<css::uno::XComponentContext> xContext(comphelper::getProcessComponentContext()); + css::uno::Reference<css::frame::XDesktop2> xDesktop(css::frame::theDesktop::get(xContext)); + css::uno::Reference<css::frame::XFrame> xFrame(xDesktop->getActiveFrame()); + if (!xFrame.is()) + return; + + OUString aModuleName; + try + { + css::uno::Reference<css::frame::XModuleManager> xModuleManager(css::frame::ModuleManager::create(xContext)); + aModuleName = xModuleManager->identify(xFrame); + } + catch( const css::uno::Exception& ) + {} + + css::uno::Sequence<css::uno::Any> aArgs { + css::uno::Any(comphelper::makePropertyValue("ModuleIdentifier", aModuleName)), + css::uno::Any(comphelper::makePropertyValue("Frame", css::uno::Any(xFrame))), + css::uno::Any(comphelper::makePropertyValue("InToolbar", css::uno::Any(true))) + }; + + const OUString aCommand(GetCommand()); + if (!aCommand.isEmpty() && GetPopupMenu()->GetItemCount() == 0) + { + css::uno::Reference<css::frame::XUIControllerFactory> xPopupMenuControllerFactory = + css::frame::thePopupMenuControllerFactory::get(xContext); + + if (xPopupMenuControllerFactory->hasController(aCommand, aModuleName)) + m_xPopupController.set(xPopupMenuControllerFactory->createInstanceWithArgumentsAndContext( + aCommand, aArgs, xContext), css::uno::UNO_QUERY); + } + + // No registered controller found, use one the can handle arbitrary menus (e.g. defined in .ui file). + if (!m_xPopupController.is()) + m_xPopupController.set(xContext->getServiceManager()->createInstanceWithArgumentsAndContext( + "com.sun.star.comp.framework.ResourceMenuController", aArgs, xContext), css::uno::UNO_QUERY); + + if (m_xPopupController.is()) + m_xPopupController->setPopupMenu(m_xPopupMenu); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/source/control/menubtn.cxx b/vcl/source/control/menubtn.cxx new file mode 100644 index 000000000..cd9b20959 --- /dev/null +++ b/vcl/source/control/menubtn.cxx @@ -0,0 +1,290 @@ +/* -*- 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 <vcl/dockwin.hxx> +#include <vcl/event.hxx> +#include <vcl/toolkit/floatwin.hxx> +#include <vcl/menu.hxx> +#include <vcl/timer.hxx> +#include <vcl/toolkit/menubtn.hxx> +#include <vcl/settings.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <vcl/uitest/logger.hxx> +#include <vcl/uitest/eventdescription.hxx> +#include <menutogglebutton.hxx> +#include <tools/json_writer.hxx> + +namespace +{ +void collectUIInformation( const OUString& aID, const OUString& aevent , const OUString& akey , const OUString& avalue) +{ + EventDescription aDescription; + aDescription.aID = aID; + aDescription.aParameters = {{ akey , avalue}}; + aDescription.aAction = aevent; + aDescription.aParent = "MainWindow"; + aDescription.aKeyWord = "MenuButton"; + UITestLogger::getInstance().logEvent(aDescription); +} +} + +void MenuButton::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + + PushButton::ImplInit( pParent, nStyle ); + EnableRTL( AllSettings::GetLayoutRTL() ); +} + +void MenuButton::ExecuteMenu() +{ + mbStartingMenu = true; + + Activate(); + + if (!mpMenu && !mpFloatingWindow) + { + mbStartingMenu = false; + return; + } + + Size aSize = GetSizePixel(); + SetPressed( true ); + EndSelection(); + if (mpMenu) + { + Point aPos(0, 1); + tools::Rectangle aRect(aPos, aSize ); + mpMenu->Execute(this, aRect, PopupMenuFlags::ExecuteDown); + + if (isDisposed()) + return; + + mnCurItemId = mpMenu->GetCurItemId(); + msCurItemIdent = mpMenu->GetCurItemIdent(); + } + else + { + Point aPos(GetParent()->OutputToScreenPixel(GetPosPixel())); + tools::Rectangle aRect(aPos, aSize ); + FloatWinPopupFlags nFlags = FloatWinPopupFlags::Down | FloatWinPopupFlags::GrabFocus; + if (mpFloatingWindow->GetType() == WindowType::FLOATINGWINDOW) + static_cast<FloatingWindow*>(mpFloatingWindow.get())->StartPopupMode(aRect, nFlags); + else + { + mpFloatingWindow->EnableDocking(); + vcl::Window::GetDockingManager()->StartPopupMode(mpFloatingWindow, aRect, nFlags); + } + } + + mbStartingMenu = false; + + SetPressed(false); + OUString aID = get_id(); // tdf#136678 take a copy if we are destroyed by Select callback + if (mnCurItemId) + { + Select(); + mnCurItemId = 0; + msCurItemIdent.clear(); + } + collectUIInformation(aID,"OPENLIST","",""); +} + +void MenuButton::CancelMenu() +{ + if (!mpMenu && !mpFloatingWindow) + return; + + if (mpMenu) + { + mpMenu->EndExecute(); + } + else + { + if (mpFloatingWindow->GetType() == WindowType::FLOATINGWINDOW) + static_cast<FloatingWindow*>(mpFloatingWindow.get())->EndPopupMode(); + else + vcl::Window::GetDockingManager()->EndPopupMode(mpFloatingWindow); + } + collectUIInformation(get_id(),"CLOSELIST","",""); +} + +bool MenuButton::InPopupMode() const +{ + if (mbStartingMenu) + return true; + + if (!mpMenu && !mpFloatingWindow) + return false; + + if (mpMenu) + return PopupMenu::GetActivePopupMenu() == mpMenu; + else + { + if (mpFloatingWindow->GetType() == WindowType::FLOATINGWINDOW) + return static_cast<const FloatingWindow*>(mpFloatingWindow.get())->IsInPopupMode(); + else + return vcl::Window::GetDockingManager()->IsInPopupMode(mpFloatingWindow); + } +} + +MenuButton::MenuButton( vcl::Window* pParent, WinBits nWinBits ) + : PushButton(WindowType::MENUBUTTON) + , mnCurItemId(0) + , mbDelayMenu(false) + , mbStartingMenu(false) +{ + mnDDStyle = PushButtonDropdownStyle::MenuButton; + ImplInit(pParent, nWinBits); +} + +MenuButton::~MenuButton() +{ + disposeOnce(); +} + +void MenuButton::dispose() +{ + mpMenuTimer.reset(); + mpFloatingWindow.clear(); + mpMenu.clear(); + PushButton::dispose(); +} + +IMPL_LINK_NOARG(MenuButton, ImplMenuTimeoutHdl, Timer *, void) +{ + // See if Button Tracking is still active, as it could've been cancelled earlier + if ( IsTracking() ) + { + if ( !(GetStyle() & WB_NOPOINTERFOCUS) ) + GrabFocus(); + ExecuteMenu(); + } +} + +void MenuButton::MouseButtonDown( const MouseEvent& rMEvt ) +{ + bool bExecute = true; + if (mbDelayMenu) + { + // If the separated dropdown symbol is not hit, delay the popup execution + if( rMEvt.GetPosPixel().X() <= ImplGetSeparatorX() ) + { + if ( !mpMenuTimer ) + { + mpMenuTimer.reset(new Timer("MenuTimer")); + mpMenuTimer->SetInvokeHandler( LINK( this, MenuButton, ImplMenuTimeoutHdl ) ); + } + + mpMenuTimer->SetTimeout( MouseSettings::GetActionDelay() ); + mpMenuTimer->Start(); + + PushButton::MouseButtonDown( rMEvt ); + bExecute = false; + } + } + if( bExecute ) + { + if ( PushButton::ImplHitTestPushButton( this, rMEvt.GetPosPixel() ) ) + { + if ( !(GetStyle() & WB_NOPOINTERFOCUS) ) + GrabFocus(); + ExecuteMenu(); + } + } +} + +void MenuButton::KeyInput( const KeyEvent& rKEvt ) +{ + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + sal_uInt16 nCode = aKeyCode.GetCode(); + if ( (nCode == KEY_DOWN) && aKeyCode.IsMod2() ) + ExecuteMenu(); + else if ( !mbDelayMenu && + !aKeyCode.GetModifier() && + ((nCode == KEY_RETURN) || (nCode == KEY_SPACE)) ) + ExecuteMenu(); + else + PushButton::KeyInput( rKEvt ); +} + +void MenuButton::Activate() +{ + maActivateHdl.Call( this ); +} + +void MenuButton::Select() +{ + if (mnCurItemId) + collectUIInformation(get_id(),"OPENFROMLIST","POS",OUString::number(mnCurItemId)); + + maSelectHdl.Call( this ); +} + +void MenuButton::SetPopupMenu(PopupMenu* pNewMenu) +{ + if (pNewMenu == mpMenu) + return; + + mpMenu = pNewMenu; +} + +void MenuButton::SetPopover(Window* pWindow) +{ + if (pWindow == mpFloatingWindow) + return; + + mpFloatingWindow = pWindow; +} + + +FactoryFunction MenuButton::GetUITestFactory() const +{ + return MenuButtonUIObject::create; +} + +void MenuButton::SetCurItemId(){ + mnCurItemId = mpMenu->GetCurItemId(); + msCurItemIdent = mpMenu->GetCurItemIdent(); +} + +//class MenuToggleButton ---------------------------------------------------- + +MenuToggleButton::MenuToggleButton( vcl::Window* pParent, WinBits nWinBits ) + : MenuButton( pParent, nWinBits ) +{ +} + +MenuToggleButton::~MenuToggleButton() +{ + disposeOnce(); +} + +void MenuToggleButton::SetActive( bool bSel ) +{ + mbIsActive = bSel; +} + +bool MenuToggleButton::GetActive() const +{ + return mbIsActive; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/notebookbar.cxx b/vcl/source/control/notebookbar.cxx new file mode 100644 index 000000000..24e619ff9 --- /dev/null +++ b/vcl/source/control/notebookbar.cxx @@ -0,0 +1,347 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/config.h> + +#include <string_view> + +#include <vcl/layout.hxx> +#include <vcl/notebookbar/notebookbar.hxx> +#include <vcl/syswin.hxx> +#include <vcl/taskpanelist.hxx> +#include <vcl/NotebookbarContextControl.hxx> +#include <cppuhelper/implbase.hxx> +#include <comphelper/processfactory.hxx> +#include <rtl/bootstrap.hxx> +#include <osl/file.hxx> +#include <config_folders.h> +#include <com/sun/star/frame/XFrame.hpp> +#include <com/sun/star/ui/ContextChangeEventMultiplexer.hpp> +#include <comphelper/lok.hxx> + +static OUString getCustomizedUIRootDir() +{ + OUString sShareLayer("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE( + "bootstrap") ":UserInstallation}/user/config/soffice.cfg/"); + rtl::Bootstrap::expandMacros(sShareLayer); + return sShareLayer; +} + +static bool doesFileExist(std::u16string_view sUIDir, std::u16string_view sUIFile) +{ + OUString sUri = OUString::Concat(sUIDir) + sUIFile; + osl::File file(sUri); + return( file.open(0) == osl::FileBase::E_None ); +} + +/** + * split from the main class since it needs different ref-counting mana + */ +class NotebookBarContextChangeEventListener : public ::cppu::WeakImplHelper<css::ui::XContextChangeEventListener> +{ + VclPtr<NotebookBar> mpParent; +public: + explicit NotebookBarContextChangeEventListener(NotebookBar *p) : mpParent(p) {} + + // XContextChangeEventListener + virtual void SAL_CALL notifyContextChangeEvent(const css::ui::ContextChangeEventObject& rEvent) override; + + virtual void SAL_CALL disposing(const ::css::lang::EventObject&) override; +}; + +NotebookBar::NotebookBar(Window* pParent, const OString& rID, const OUString& rUIXMLDescription, + const css::uno::Reference<css::frame::XFrame>& rFrame, + const NotebookBarAddonsItem& aNotebookBarAddonsItem) + : Control(pParent) + , m_pEventListener(new NotebookBarContextChangeEventListener(this)) + , m_pViewShell(nullptr) + , m_bIsWelded(false) + , m_sUIXMLDescription(rUIXMLDescription) +{ + mxFrame = rFrame; + + SetStyle(GetStyle() | WB_DIALOGCONTROL); + OUString sUIDir = AllSettings::GetUIRootDir(); + bool doesCustomizedUIExist = doesFileExist(getCustomizedUIRootDir(), rUIXMLDescription); + if ( doesCustomizedUIExist ) + sUIDir = getCustomizedUIRootDir(); + + bool bIsWelded = comphelper::LibreOfficeKit::isActive(); + if (bIsWelded) + { + m_bIsWelded = true; + m_xVclContentArea = VclPtr<VclVBox>::Create(this); + m_xVclContentArea->Show(); + // now access it using GetMainContainer and set dispose callback with SetDisposeCallback + } + else + { + m_pUIBuilder.reset( + new VclBuilder(this, sUIDir, rUIXMLDescription, rID, rFrame, true, &aNotebookBarAddonsItem)); + + // In the Notebookbar's .ui file must exist control handling context + // - implementing NotebookbarContextControl interface with id "ContextContainer" + // or "ContextContainerX" where X is a number >= 1 + NotebookbarContextControl* pContextContainer = nullptr; + int i = 0; + do + { + OUString aName = "ContextContainer"; + if (i) + aName += OUString::number(i); + + pContextContainer = dynamic_cast<NotebookbarContextControl*>(m_pUIBuilder->get<Window>(OUStringToOString(aName, RTL_TEXTENCODING_UTF8))); + if (pContextContainer) + m_pContextContainers.push_back(pContextContainer); + i++; + } + while( pContextContainer != nullptr ); + } + + UpdateBackground(); +} + +void NotebookBar::SetDisposeCallback(const Link<const SfxViewShell*, void> rDisposeCallback, const SfxViewShell* pViewShell) +{ + m_rDisposeLink = rDisposeCallback; + m_pViewShell = pViewShell; +} + +NotebookBar::~NotebookBar() +{ + disposeOnce(); +} + +void NotebookBar::dispose() +{ + m_pContextContainers.clear(); + if (m_pSystemWindow && m_pSystemWindow->ImplIsInTaskPaneList(this)) + m_pSystemWindow->GetTaskPaneList()->RemoveWindow(this); + m_pSystemWindow.clear(); + + if (m_rDisposeLink.IsSet()) + m_rDisposeLink.Call(m_pViewShell); + + if (m_bIsWelded) + m_xVclContentArea.disposeAndClear(); + else + disposeBuilder(); + + assert(m_alisteningControllers.empty()); + m_pEventListener.clear(); + + Control::dispose(); +} + +bool NotebookBar::PreNotify(NotifyEvent& rNEvt) +{ + // capture KeyEvents for taskpane cycling + if (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) + { + if (m_pSystemWindow) + return m_pSystemWindow->PreNotify(rNEvt); + } + return Window::PreNotify( rNEvt ); +} + +Size NotebookBar::GetOptimalSize() const +{ + if (isLayoutEnabled(this)) + return VclContainer::getLayoutRequisition(*GetWindow(GetWindowType::FirstChild)); + + return Control::GetOptimalSize(); +} + +void NotebookBar::setPosSizePixel(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, PosSizeFlags nFlags) +{ + bool bCanHandleSmallerWidth = false; + bool bCanHandleSmallerHeight = false; + + bool bIsLayoutEnabled = isLayoutEnabled(this); + Window *pChild = GetWindow(GetWindowType::FirstChild); + + if (bIsLayoutEnabled && pChild->GetType() == WindowType::SCROLLWINDOW) + { + WinBits nStyle = pChild->GetStyle(); + if (nStyle & (WB_AUTOHSCROLL | WB_HSCROLL)) + bCanHandleSmallerWidth = true; + if (nStyle & (WB_AUTOVSCROLL | WB_VSCROLL)) + bCanHandleSmallerHeight = true; + } + + Size aSize(GetOptimalSize()); + if (!bCanHandleSmallerWidth) + nWidth = std::max(nWidth, aSize.Width()); + if (!bCanHandleSmallerHeight) + nHeight = std::max(nHeight, aSize.Height()); + + Control::setPosSizePixel(nX, nY, nWidth, nHeight, nFlags); + + if (bIsLayoutEnabled && (nFlags & PosSizeFlags::Size)) + VclContainer::setLayoutAllocation(*pChild, Point(0, 0), Size(nWidth, nHeight)); +} + +void NotebookBar::Resize() +{ + if(m_pUIBuilder && m_pUIBuilder->get_widget_root()) + { + vcl::Window* pWindow = m_pUIBuilder->get_widget_root()->GetChild(0); + if (pWindow) + { + Size aSize = pWindow->GetSizePixel(); + aSize.setWidth( GetSizePixel().Width() ); + pWindow->SetSizePixel(aSize); + } + } + if(m_bIsWelded) + { + vcl::Window* pChild = GetWindow(GetWindowType::FirstChild); + assert(pChild); + VclContainer::setLayoutAllocation(*pChild, Point(0, 0), GetSizePixel()); + Control::Resize(); + } + Control::Resize(); +} + +void NotebookBar::SetSystemWindow(SystemWindow* pSystemWindow) +{ + m_pSystemWindow = pSystemWindow; + if (!m_pSystemWindow->ImplIsInTaskPaneList(this)) + m_pSystemWindow->GetTaskPaneList()->AddWindow(this); +} + +void SAL_CALL NotebookBarContextChangeEventListener::notifyContextChangeEvent(const css::ui::ContextChangeEventObject& rEvent) +{ + if (mpParent) + { + for (NotebookbarContextControl* pControl : mpParent->m_pContextContainers) + pControl->SetContext(vcl::EnumContext::GetContextEnum(rEvent.ContextName)); + } +} + +void NotebookBar::ControlListenerForCurrentController(bool bListen) +{ + if (comphelper::LibreOfficeKit::isActive()) + return; + + auto xController = mxFrame->getController(); + if(bListen) + { + // add listeners + if (m_alisteningControllers.count(xController) == 0) + { + auto xMultiplexer(css::ui::ContextChangeEventMultiplexer::get( + ::comphelper::getProcessComponentContext())); + xMultiplexer->addContextChangeEventListener(m_pEventListener, xController); + m_alisteningControllers.insert(xController); + } + } + else + { + // remove listeners + if (m_alisteningControllers.count(xController)) + { + auto xMultiplexer(css::ui::ContextChangeEventMultiplexer::get( + ::comphelper::getProcessComponentContext())); + xMultiplexer->removeContextChangeEventListener(m_pEventListener, xController); + m_alisteningControllers.erase(xController); + } + } +} + +void NotebookBar::StopListeningAllControllers() +{ + if (comphelper::LibreOfficeKit::isActive()) + return; + + auto xMultiplexer( + css::ui::ContextChangeEventMultiplexer::get(comphelper::getProcessComponentContext())); + xMultiplexer->removeAllContextChangeEventListeners(m_pEventListener); + m_alisteningControllers.clear(); +} + +void SAL_CALL NotebookBarContextChangeEventListener::disposing(const ::css::lang::EventObject&) +{ + mpParent.clear(); +} + +void NotebookBar::DataChanged(const DataChangedEvent& rDCEvt) +{ + UpdateBackground(); + Control::DataChanged(rDCEvt); +} + +void NotebookBar::StateChanged(const StateChangedType nStateChange ) +{ + UpdateBackground(); + Control::StateChanged(nStateChange); + Invalidate(); +} + +void NotebookBar::UpdateBackground() +{ + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + const BitmapEx& aPersona = rStyleSettings.GetPersonaHeader(); + Wallpaper aWallpaper(aPersona); + aWallpaper.SetStyle(WallpaperStyle::TopRight); + if (!aPersona.IsEmpty()) + { + SetBackground(aWallpaper); + UpdatePersonaSettings(); + GetOutDev()->SetSettings( PersonaSettings ); + } + else + { + SetBackground(rStyleSettings.GetDialogColor()); + UpdateDefaultSettings(); + GetOutDev()->SetSettings( DefaultSettings ); + } + + Invalidate(tools::Rectangle(Point(0,0), GetSizePixel())); +} + +void NotebookBar::UpdateDefaultSettings() +{ + AllSettings aAllSettings( GetSettings() ); + StyleSettings aStyleSet( aAllSettings.GetStyleSettings() ); + + ::Color aTextColor = aStyleSet.GetFieldTextColor(); + aStyleSet.SetDialogTextColor( aTextColor ); + aStyleSet.SetButtonTextColor( aTextColor ); + aStyleSet.SetRadioCheckTextColor( aTextColor ); + aStyleSet.SetGroupTextColor( aTextColor ); + aStyleSet.SetLabelTextColor( aTextColor ); + aStyleSet.SetWindowTextColor( aTextColor ); + aStyleSet.SetTabTextColor(aTextColor); + aStyleSet.SetToolTextColor(aTextColor); + + aAllSettings.SetStyleSettings(aStyleSet); + DefaultSettings = aAllSettings; +} + +void NotebookBar::UpdatePersonaSettings() +{ + AllSettings aAllSettings( GetSettings() ); + StyleSettings aStyleSet( aAllSettings.GetStyleSettings() ); + + ::Color aTextColor = aStyleSet.GetPersonaMenuBarTextColor().value_or(COL_BLACK ); + aStyleSet.SetDialogTextColor( aTextColor ); + aStyleSet.SetButtonTextColor( aTextColor ); + aStyleSet.SetRadioCheckTextColor( aTextColor ); + aStyleSet.SetGroupTextColor( aTextColor ); + aStyleSet.SetLabelTextColor( aTextColor ); + aStyleSet.SetWindowTextColor( aTextColor ); + aStyleSet.SetTabTextColor(aTextColor); + aStyleSet.SetToolTextColor(aTextColor); + + aAllSettings.SetStyleSettings(aStyleSet); + PersonaSettings = aAllSettings; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/prgsbar.cxx b/vcl/source/control/prgsbar.cxx new file mode 100644 index 000000000..6b7a7007c --- /dev/null +++ b/vcl/source/control/prgsbar.cxx @@ -0,0 +1,232 @@ +/* -*- 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 <vcl/event.hxx> +#include <vcl/status.hxx> +#include <vcl/toolkit/prgsbar.hxx> +#include <vcl/settings.hxx> +#include <sal/log.hxx> +#include <vcl/svapp.hxx> +#include <vcl/idle.hxx> + +#define PROGRESSBAR_OFFSET 3 +#define PROGRESSBAR_WIN_OFFSET 2 + +void ProgressBar::ImplInit() +{ + mnPrgsWidth = 0; + mnPrgsHeight = 0; + mnPercent = 0; + mnPercentCount = 0; + mbCalcNew = true; + + ImplInitSettings( true, true, true ); +} + +static WinBits clearProgressBarBorder( vcl::Window const * pParent, WinBits nOrgStyle ) +{ + WinBits nOutStyle = nOrgStyle; + if( pParent && (nOrgStyle & WB_BORDER) != 0 ) + { + if( pParent->IsNativeControlSupported( ControlType::Progress, ControlPart::Entire ) ) + nOutStyle &= WB_BORDER; + } + return nOutStyle; +} + +Size ProgressBar::GetOptimalSize() const +{ + return Size(150, 20); +} + +ProgressBar::ProgressBar( vcl::Window* pParent, WinBits nWinStyle ) : + Window( pParent, clearProgressBarBorder( pParent, nWinStyle ) ) +{ + SetOutputSizePixel( GetOptimalSize() ); + ImplInit(); +} + +void ProgressBar::ImplInitSettings( bool bFont, + bool bForeground, bool bBackground ) +{ + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + +/* FIXME: !!! We do not support text output at the moment + if ( bFont ) + ApplyControlFont(*this, rStyleSettings.GetAppFont()); +*/ + + if ( bBackground ) + { + if( !IsControlBackground() && + IsNativeControlSupported( ControlType::Progress, ControlPart::Entire ) ) + { + if( GetStyle() & WB_BORDER ) + SetBorderStyle( WindowBorderStyle::REMOVEBORDER ); + EnableChildTransparentMode(); + SetPaintTransparent( true ); + SetBackground(); + SetParentClipMode( ParentClipMode::NoClip ); + } + else + { + Color aColor; + if ( IsControlBackground() ) + aColor = GetControlBackground(); + else + aColor = rStyleSettings.GetFaceColor(); + SetBackground( aColor ); + } + } + + if ( !(bForeground || bFont) ) + return; + + Color aColor = rStyleSettings.GetHighlightColor(); + if ( IsControlForeground() ) + aColor = GetControlForeground(); + if ( aColor.IsRGBEqual( GetBackground().GetColor() ) ) + { + if ( aColor.GetLuminance() > 100 ) + aColor.DecreaseLuminance( 64 ); + else + aColor.IncreaseLuminance( 64 ); + } + GetOutDev()->SetLineColor(); + GetOutDev()->SetFillColor( aColor ); +/* FIXME: !!! We do not support text output at the moment + SetTextColor( aColor ); + SetTextFillColor(); +*/ +} + +void ProgressBar::ImplDrawProgress(vcl::RenderContext& rRenderContext, sal_uInt16 nNewPerc) +{ + if (mbCalcNew) + { + mbCalcNew = false; + + Size aSize(GetOutputSizePixel()); + mnPrgsHeight = aSize.Height() - (PROGRESSBAR_WIN_OFFSET * 2); + mnPrgsWidth = (mnPrgsHeight * 2) / 3; + maPos.setY( PROGRESSBAR_WIN_OFFSET ); + tools::Long nMaxWidth = aSize.Width() - (PROGRESSBAR_WIN_OFFSET * 2) + PROGRESSBAR_OFFSET; + sal_uInt16 nMaxCount = static_cast<sal_uInt16>(nMaxWidth / (mnPrgsWidth+PROGRESSBAR_OFFSET)); + if (nMaxCount <= 1) + { + nMaxCount = 1; + } + else + { + while (((10000 / (10000 / nMaxCount)) * (mnPrgsWidth + PROGRESSBAR_OFFSET)) > nMaxWidth) + { + nMaxCount--; + } + } + mnPercentCount = 10000 / nMaxCount; + nMaxWidth = ((10000 / (10000 / nMaxCount)) * (mnPrgsWidth + PROGRESSBAR_OFFSET)) - PROGRESSBAR_OFFSET; + maPos.setX( (aSize.Width() - nMaxWidth) / 2 ); + } + + ::DrawProgress(this, rRenderContext, maPos, PROGRESSBAR_OFFSET, mnPrgsWidth, mnPrgsHeight, + /*nPercent1=*/0, nNewPerc * 100, mnPercentCount, + tools::Rectangle(Point(), GetSizePixel())); +} + +void ProgressBar::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& /*rRect*/) +{ + ImplDrawProgress(rRenderContext, mnPercent); +} + +void ProgressBar::Resize() +{ + mbCalcNew = true; + if ( IsReallyVisible() ) + Invalidate(); +} + +void ProgressBar::SetValue( sal_uInt16 nNewPercent ) +{ + SAL_WARN_IF( nNewPercent > 100, "vcl", "StatusBar::SetProgressValue(): nPercent > 100" ); + + if ( nNewPercent < mnPercent ) + { + mbCalcNew = true; + mnPercent = nNewPercent; + if ( IsReallyVisible() ) + { + Invalidate(); + PaintImmediately(); + } + } + else if ( mnPercent != nNewPercent ) + { + mnPercent = nNewPercent; + Invalidate(); + + // Make sure the progressbar is actually painted even if the caller is busy with its task, + // so the main loop would not be invoked. + Idle aIdle("ProgressBar::SetValue aIdle"); + aIdle.SetPriority(TaskPriority::POST_PAINT); + aIdle.Start(); + while (aIdle.IsActive() && !Application::IsQuit()) + { + Application::Yield(); + } + } +} + +void ProgressBar::StateChanged( StateChangedType nType ) +{ +/* FIXME: !!! We do not support text output at the moment + if ( (nType == StateChangedType::Zoom) || + (nType == StateChangedType::ControlFont) ) + { + ImplInitSettings( true, false, false ); + Invalidate(); + } + else +*/ + if ( nType == StateChangedType::ControlForeground ) + { + ImplInitSettings( false, true, false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ImplInitSettings( false, false, true ); + Invalidate(); + } + + Window::StateChanged( nType ); +} + +void ProgressBar::DataChanged( const DataChangedEvent& rDCEvt ) +{ + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) ) + { + ImplInitSettings( true, true, true ); + Invalidate(); + } + + Window::DataChanged( rDCEvt ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/quickselectionengine.cxx b/vcl/source/control/quickselectionengine.cxx new file mode 100644 index 000000000..777e00e0b --- /dev/null +++ b/vcl/source/control/quickselectionengine.cxx @@ -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 . + */ + +#include <vcl/quickselectionengine.hxx> +#include <vcl/event.hxx> +#include <vcl/timer.hxx> +#include <vcl/i18nhelp.hxx> +#include <vcl/svapp.hxx> +#include <vcl/settings.hxx> +#include <sal/log.hxx> + +#include <optional> + +namespace vcl +{ + + struct QuickSelectionEngine_Data + { + ISearchableStringList& rEntryList; + OUString sCurrentSearchString; + ::std::optional< sal_Unicode > aSingleSearchChar; + Timer aSearchTimeout; + + explicit QuickSelectionEngine_Data( ISearchableStringList& _entryList ) + :rEntryList( _entryList ) + ,aSearchTimeout( "vcl::QuickSelectionEngine_Data aSearchTimeout" ) + { + aSearchTimeout.SetTimeout( 2500 ); + aSearchTimeout.SetInvokeHandler( LINK( this, QuickSelectionEngine_Data, SearchStringTimeout ) ); + } + + ~QuickSelectionEngine_Data() + { + aSearchTimeout.Stop(); + } + + DECL_LINK( SearchStringTimeout, Timer*, void ); + }; + + namespace + { + void lcl_reset( QuickSelectionEngine_Data& _data ) + { + _data.sCurrentSearchString.clear(); + _data.aSingleSearchChar.reset(); + _data.aSearchTimeout.Stop(); + } + } + + IMPL_LINK_NOARG( QuickSelectionEngine_Data, SearchStringTimeout, Timer*, void ) + { + lcl_reset( *this ); + } + + static StringEntryIdentifier findMatchingEntry( const OUString& _searchString, QuickSelectionEngine_Data const & _engineData ) + { + const vcl::I18nHelper& rI18nHelper = Application::GetSettings().GetLocaleI18nHelper(); + // TODO: do we really need the Window's settings here? The original code used it ... + + OUString sEntryText; + // get the "current + 1" entry + StringEntryIdentifier pSearchEntry = _engineData.rEntryList.CurrentEntry( sEntryText ); + if ( pSearchEntry ) + pSearchEntry = _engineData.rEntryList.NextEntry( pSearchEntry, sEntryText ); + // loop 'til we find another matching entry + StringEntryIdentifier pStartedWith = pSearchEntry; + while ( pSearchEntry ) + { + if ( rI18nHelper.MatchString( _searchString, sEntryText ) ) + break; + + pSearchEntry = _engineData.rEntryList.NextEntry( pSearchEntry, sEntryText ); + if ( pSearchEntry == pStartedWith ) + pSearchEntry = nullptr; + } + + return pSearchEntry; + } + + QuickSelectionEngine::QuickSelectionEngine( ISearchableStringList& _entryList ) + :m_pData( new QuickSelectionEngine_Data( _entryList ) ) + { + } + + QuickSelectionEngine::~QuickSelectionEngine() + { + } + + bool QuickSelectionEngine::HandleKeyEvent( const KeyEvent& _keyEvent ) + { + sal_Unicode c = _keyEvent.GetCharCode(); + + if ( ( c >= 32 ) && ( c != 127 ) && !_keyEvent.GetKeyCode().IsMod2() ) + { + m_pData->sCurrentSearchString += OUStringChar(c); + SAL_INFO( "vcl", "QuickSelectionEngine::HandleKeyEvent: searching for " << m_pData->sCurrentSearchString ); + + if ( m_pData->sCurrentSearchString.getLength() == 1 ) + { // first character in the search -> remember + m_pData->aSingleSearchChar = c; + } + else if ( m_pData->sCurrentSearchString.getLength() > 1 ) + { + if ( !!m_pData->aSingleSearchChar && ( *m_pData->aSingleSearchChar != c ) ) + // we already have a "single char", but the current one is different -> reset + m_pData->aSingleSearchChar.reset(); + } + + OUString aSearchTemp( m_pData->sCurrentSearchString ); + + StringEntryIdentifier pMatchingEntry = findMatchingEntry( aSearchTemp, *m_pData ); + SAL_INFO( "vcl", "QuickSelectionEngine::HandleKeyEvent: found " << pMatchingEntry ); + if ( !pMatchingEntry && (aSearchTemp.getLength() > 1) && !!m_pData->aSingleSearchChar ) + { + // if there's only one letter in the search string, use a different search mode + aSearchTemp = OUString(*m_pData->aSingleSearchChar); + pMatchingEntry = findMatchingEntry( aSearchTemp, *m_pData ); + } + + if ( pMatchingEntry ) + { + m_pData->rEntryList.SelectEntry( pMatchingEntry ); + m_pData->aSearchTimeout.Start(); + } + else + { + lcl_reset( *m_pData ); + } + + return true; + } + return false; + } + + void QuickSelectionEngine::Reset() + { + lcl_reset( *m_pData ); + } + +} // namespace vcl + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/roadmap.cxx b/vcl/source/control/roadmap.cxx new file mode 100644 index 000000000..7142c4dac --- /dev/null +++ b/vcl/source/control/roadmap.cxx @@ -0,0 +1,845 @@ +/* -*- 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 <vector> +#include <algorithm> +#include <o3tl/safeint.hxx> +#include <vcl/event.hxx> +#include <vcl/toolkit/roadmap.hxx> +#include <vcl/settings.hxx> +#include <vcl/vclevent.hxx> +#include <hyperlabel.hxx> +#include <tools/color.hxx> +#include <rtl/ustring.hxx> + +constexpr tools::Long LABELBASEMAPHEIGHT = 8; +constexpr tools::Long ROADMAP_INDENT_X = 4; +constexpr tools::Long ROADMAP_INDENT_Y = 27; +constexpr tools::Long ROADMAP_ITEM_DISTANCE_Y = 6; + +namespace vcl +{ + +typedef std::vector< RoadmapItem* > HL_Vector; + +//= ColorChanger + +namespace { + +class IDLabel : public FixedText +{ +public: + IDLabel( vcl::Window* _pParent, WinBits _nWinStyle ); + virtual void DataChanged( const DataChangedEvent& rDCEvt ) override; + virtual void ApplySettings(vcl::RenderContext& rRenderContext) override; +}; + +} + +class RoadmapItem : public RoadmapTypes +{ +private: + VclPtr<IDLabel> mpID; + VclPtr<HyperLabel> mpDescription; + const Size m_aItemPlayground; + +public: + RoadmapItem( ORoadmap& _rParent, const Size& _rItemPlayground ); + ~RoadmapItem(); + + void SetID( sal_Int16 ID ); + sal_Int16 GetID() const; + + void SetIndex( ItemIndex Index ); + ItemIndex GetIndex() const; + + void Update( ItemIndex RMIndex, const OUString& _rText ); + + void SetPosition( RoadmapItem const * OldHyperLabel ); + + void ToggleBackgroundColor( const Color& _rGBColor ); + void SetInteractive( bool _bInteractive ); + + void SetClickHdl( const Link<HyperLabel*,void>& rLink ); + void Enable( bool bEnable ); + bool IsEnabled() const; + void GrabFocus(); + + bool Contains( const vcl::Window* _pWindow ) const; + +private: + void ImplUpdateIndex( const ItemIndex _nIndex ); + void ImplUpdatePosSize(); +}; + +//= RoadmapImpl + +class RoadmapImpl : public RoadmapTypes +{ +protected: + const ORoadmap& m_rAntiImpl; + Link<LinkParamNone*,void> m_aSelectHdl; + BitmapEx m_aPicture; + HL_Vector m_aRoadmapSteps; + ItemId m_iCurItemID; + bool m_bInteractive : 1; + bool m_bComplete : 1; + Size m_aItemSizePixel; +public: + bool m_bPaintInitialized : 1; + +public: + explicit RoadmapImpl(const ORoadmap& rAntiImpl) + : m_rAntiImpl(rAntiImpl) + , m_iCurItemID(-1) + , m_bInteractive(true) + , m_bComplete(true) + , m_bPaintInitialized(false) + , InCompleteHyperLabel(nullptr) + {} + + RoadmapItem* InCompleteHyperLabel; + + HL_Vector& getHyperLabels() + { + return m_aRoadmapSteps; + } + + void insertHyperLabel(ItemIndex Index, RoadmapItem* _rRoadmapStep) + { + m_aRoadmapSteps.insert(m_aRoadmapSteps.begin() + Index, _rRoadmapStep); + } + + ItemIndex getItemCount() const + { + return m_aRoadmapSteps.size(); + } + + void setCurItemID(ItemId i) + { + m_iCurItemID = i; + } + ItemId getCurItemID() const + { + return m_iCurItemID; + } + + void setInteractive(const bool _bInteractive) + { + m_bInteractive = _bInteractive; + } + bool isInteractive() const + { + return m_bInteractive; + } + + void setComplete(const bool _bComplete) + { + m_bComplete = _bComplete; + } + bool isComplete() const + { + return m_bComplete; + } + + void setPicture(const BitmapEx& _rPic) + { + m_aPicture = _rPic; + } + const BitmapEx& getPicture() const + { + return m_aPicture; + } + + void setSelectHdl(const Link<LinkParamNone*,void>& _rHdl) + { + m_aSelectHdl = _rHdl; + } + const Link<LinkParamNone*,void>& getSelectHdl() const + { + return m_aSelectHdl; + } + + void initItemSize(); + const Size& getItemSize() const + { + return m_aItemSizePixel; + } + + void removeHyperLabel(ItemIndex Index) + { + if ((Index > -1) && (Index < getItemCount())) + { + delete m_aRoadmapSteps[Index]; + m_aRoadmapSteps.erase(m_aRoadmapSteps.begin() + Index); + } + } +}; + +void RoadmapImpl::initItemSize() +{ + Size aLabelSize( m_rAntiImpl.GetOutputSizePixel() ); + aLabelSize.setHeight( m_rAntiImpl.LogicToPixel(Size(0, LABELBASEMAPHEIGHT), MapMode(MapUnit::MapAppFont)).Height() ); + aLabelSize.AdjustWidth( -(m_rAntiImpl.LogicToPixel(Size(2 * ROADMAP_INDENT_X, 0), MapMode(MapUnit::MapAppFont)).Width()) ); + m_aItemSizePixel = aLabelSize; +} + +//= Roadmap + +ORoadmap::ORoadmap(vcl::Window* _pParent, WinBits _nWinStyle) + : Control(_pParent, _nWinStyle) + , m_pImpl(new RoadmapImpl(*this)) +{ +} + +void ORoadmap::implInit(vcl::RenderContext& rRenderContext) +{ + delete m_pImpl->InCompleteHyperLabel; + m_pImpl->InCompleteHyperLabel = nullptr; + m_pImpl->setCurItemID(-1); + m_pImpl->setComplete(true); + m_pImpl->m_bPaintInitialized = true; + + // Roadmap control should be reachable as one unit with a Tab key + // the next Tab key should spring out of the control. + // To reach it the control itself should get focus and set it + // on entries. The entries themself should not be reachable with + // the Tab key directly. So each entry should have WB_NOTABSTOP. + + // In other words the creator should create the control with the following + // flags: + // SetStyle( ( GetStyle() | WB_TABSTOP ) & ~WB_DIALOGCONTROL ); + +// TODO: if somebody sets a new font from outside (OutputDevice::SetFont), we would have to react +// on this with calculating a new bold font. +// Unfortunately, the OutputDevice does not offer a notify mechanism for a changed font. +// So settings the font from outside is simply a forbidden scenario at the moment + rRenderContext.EnableMapMode(false); +} + +ORoadmap::~ORoadmap() +{ + disposeOnce(); +} + +void ORoadmap::dispose() +{ + HL_Vector aItemsCopy = m_pImpl->getHyperLabels(); + m_pImpl->getHyperLabels().clear(); + for (auto const& itemCopy : aItemsCopy) + { + delete itemCopy; + } + if ( ! m_pImpl->isComplete() ) + delete m_pImpl->InCompleteHyperLabel; + m_pImpl.reset(); + Control::dispose(); +} + +RoadmapTypes::ItemId ORoadmap::GetCurrentRoadmapItemID() const +{ + return m_pImpl->getCurItemID(); +} + +RoadmapItem* ORoadmap::GetPreviousHyperLabel(ItemIndex Index) +{ + RoadmapItem* pOldItem = nullptr; + if ( Index > 0 ) + pOldItem = m_pImpl->getHyperLabels().at( Index - 1 ); + return pOldItem; +} + +RoadmapItem* ORoadmap::InsertHyperLabel(ItemIndex Index, const OUString& _sLabel, ItemId RMID, bool _bEnabled, bool _bIncomplete) +{ + if (m_pImpl->getItemCount() == 0) + m_pImpl->initItemSize(); + + RoadmapItem* pItem = nullptr; + RoadmapItem* pOldItem = GetPreviousHyperLabel( Index ); + + pItem = new RoadmapItem( *this, m_pImpl->getItemSize() ); + if ( _bIncomplete ) + { + pItem->SetInteractive( false ); + } + else + { + pItem->SetInteractive( m_pImpl->isInteractive() ); + m_pImpl->insertHyperLabel( Index, pItem ); + } + pItem->SetPosition( pOldItem ); + pItem->Update( Index, _sLabel ); + pItem->SetClickHdl(LINK( this, ORoadmap, ImplClickHdl ) ); + pItem->SetID( RMID ); + pItem->SetIndex( Index ); + if (!_bEnabled) + pItem->Enable( _bEnabled ); + return pItem; +} + +void ORoadmap::SetRoadmapBitmap(const BitmapEx& _rBmp) +{ + m_pImpl->setPicture( _rBmp ); + Invalidate( ); +} + +void ORoadmap::SetRoadmapInteractive(bool _bInteractive) +{ + m_pImpl->setInteractive( _bInteractive ); + + const HL_Vector& rItems = m_pImpl->getHyperLabels(); + for (auto const& item : rItems) + { + item->SetInteractive( _bInteractive ); + } +} + +bool ORoadmap::IsRoadmapInteractive() const +{ + return m_pImpl->isInteractive(); +} + +void ORoadmap::SetRoadmapComplete(bool _bComplete) +{ + bool bWasComplete = m_pImpl->isComplete(); + m_pImpl->setComplete( _bComplete ); + if (_bComplete) + { + if (m_pImpl->InCompleteHyperLabel != nullptr) + { + delete m_pImpl->InCompleteHyperLabel; + m_pImpl->InCompleteHyperLabel = nullptr; + } + } + else if (bWasComplete) + m_pImpl->InCompleteHyperLabel = InsertHyperLabel(m_pImpl->getItemCount(), "...", -1, true/*bEnabled*/, true/*bIncomplete*/ ); +} + +void ORoadmap::UpdatefollowingHyperLabels(ItemIndex _nIndex) +{ + const HL_Vector& rItems = m_pImpl->getHyperLabels(); + if ( _nIndex < static_cast<ItemIndex>(rItems.size()) ) + { + for ( HL_Vector::const_iterator i = rItems.begin() + _nIndex; + i != rItems.end(); + ++i, ++_nIndex + ) + { + RoadmapItem* pItem = *i; + + pItem->SetIndex( _nIndex ); + pItem->SetPosition( GetPreviousHyperLabel( _nIndex ) ); + } + + } + if ( ! m_pImpl->isComplete() ) + { + RoadmapItem* pOldItem = GetPreviousHyperLabel( m_pImpl->getItemCount() ); + m_pImpl->InCompleteHyperLabel->SetPosition( pOldItem ); + m_pImpl->InCompleteHyperLabel->Update( m_pImpl->getItemCount(), "..." ); + } +} + +void ORoadmap::ReplaceRoadmapItem(ItemIndex Index, const OUString& roadmapItem, ItemId RMID, bool _bEnabled) +{ + RoadmapItem* pItem = GetByIndex( Index); + if ( pItem != nullptr ) + { + pItem->Update( Index, roadmapItem ); + pItem->SetID( RMID ); + pItem->Enable( _bEnabled ); + } +} + +RoadmapTypes::ItemIndex ORoadmap::GetItemCount() const +{ + return m_pImpl->getItemCount(); +} + +RoadmapTypes::ItemId ORoadmap::GetItemID(ItemIndex _nIndex) const +{ + const RoadmapItem* pHyperLabel = GetByIndex( _nIndex ); + if ( pHyperLabel ) + return pHyperLabel->GetID(); + return -1; +} + +void ORoadmap::InsertRoadmapItem(ItemIndex Index, const OUString& RoadmapItem, ItemId _nUniqueId, bool _bEnabled) +{ + InsertHyperLabel( Index, RoadmapItem, _nUniqueId, _bEnabled, false/*bIncomplete*/ ); + // TODO YPos is superfluous, if items are always appended + UpdatefollowingHyperLabels( Index + 1 ); +} + +void ORoadmap::DeleteRoadmapItem(ItemIndex Index) +{ + if ( m_pImpl->getItemCount() > 0 && ( Index > -1) && ( Index < m_pImpl->getItemCount() ) ) + { + m_pImpl->removeHyperLabel( Index ); + UpdatefollowingHyperLabels( Index ); + } +} + +bool ORoadmap::IsRoadmapComplete() const +{ + return m_pImpl->isComplete(); +} + +void ORoadmap::EnableRoadmapItem( ItemId _nItemId, bool _bEnable ) +{ + RoadmapItem* pItem = GetByID( _nItemId ); + if ( pItem != nullptr ) + pItem->Enable( _bEnable ); +} + +void ORoadmap::ChangeRoadmapItemLabel( ItemId _nID, const OUString& _sLabel ) +{ + RoadmapItem* pItem = GetByID( _nID ); + if ( pItem == nullptr ) + return; + + pItem->Update( pItem->GetIndex(), _sLabel ); + + const HL_Vector& rItems = m_pImpl->getHyperLabels(); + size_t nPos = 0; + for (auto const& item : rItems) + { + item->SetPosition( GetPreviousHyperLabel(nPos) ); + ++nPos; + } +} + +void ORoadmap::ChangeRoadmapItemID(ItemId _nID, ItemId NewID) +{ + RoadmapItem* pItem = GetByID( _nID ); + if ( pItem != nullptr ) + pItem->SetID( NewID ); +} + +RoadmapItem* ORoadmap::GetByID(ItemId _nID) +{ + ItemId nLocID = 0; + const HL_Vector& rItems = m_pImpl->getHyperLabels(); + for (auto const& item : rItems) + { + nLocID = item->GetID(); + if ( nLocID == _nID ) + return item; + } + return nullptr; +} + +const RoadmapItem* ORoadmap::GetByID(ItemId _nID) const +{ + return const_cast< ORoadmap* >( this )->GetByID( _nID ); +} + +RoadmapItem* ORoadmap::GetByIndex(ItemIndex _nItemIndex) +{ + const HL_Vector& rItems = m_pImpl->getHyperLabels(); + if ( ( _nItemIndex > -1 ) && ( o3tl::make_unsigned(_nItemIndex) < rItems.size() ) ) + { + return rItems.at( _nItemIndex ); + } + return nullptr; +} + +const RoadmapItem* ORoadmap::GetByIndex(ItemIndex _nItemIndex) const +{ + return const_cast< ORoadmap* >( this )->GetByIndex( _nItemIndex ); +} + +RoadmapTypes::ItemId ORoadmap::GetNextAvailableItemId(ItemIndex _nNewIndex) +{ + ItemIndex searchIndex = ++_nNewIndex; + while ( searchIndex < m_pImpl->getItemCount() ) + { + RoadmapItem* pItem = GetByIndex( searchIndex ); + if ( pItem->IsEnabled() ) + return pItem->GetID( ); + + ++searchIndex; + } + return -1; +} + +RoadmapTypes::ItemId ORoadmap::GetPreviousAvailableItemId(ItemIndex _nNewIndex) +{ + ItemIndex searchIndex = --_nNewIndex; + while ( searchIndex > -1 ) + { + RoadmapItem* pItem = GetByIndex( searchIndex ); + if ( pItem->IsEnabled() ) + return pItem->GetID( ); + + searchIndex--; + } + return -1; +} + +void ORoadmap::DeselectOldRoadmapItems() +{ + const HL_Vector& rItems = m_pImpl->getHyperLabels(); + for (auto const& item : rItems) + { + item->ToggleBackgroundColor( COL_TRANSPARENT ); + } +} + +void ORoadmap::SetItemSelectHdl(const Link<LinkParamNone*,void>& _rHdl) +{ + m_pImpl->setSelectHdl(_rHdl); +} + +Link<LinkParamNone*,void> const & ORoadmap::GetItemSelectHdl() const +{ + return m_pImpl->getSelectHdl(); +} + +void ORoadmap::Select() +{ + GetItemSelectHdl().Call( nullptr ); + CallEventListeners( VclEventId::RoadmapItemSelected ); +} + +void ORoadmap::GetFocus() +{ + RoadmapItem* pCurHyperLabel = GetByID( GetCurrentRoadmapItemID() ); + if ( pCurHyperLabel != nullptr ) + pCurHyperLabel->GrabFocus(); +} + +bool ORoadmap::SelectRoadmapItemByID(ItemId _nNewID, bool bGrabFocus) +{ + DeselectOldRoadmapItems(); + RoadmapItem* pItem = GetByID( _nNewID ); + if ( pItem != nullptr ) + { + if ( pItem->IsEnabled() ) + { + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + pItem->ToggleBackgroundColor( rStyleSettings.GetHighlightColor() ); //HighlightColor + + if (bGrabFocus) + pItem->GrabFocus(); + m_pImpl->setCurItemID(_nNewID); + + Select(); + return true; + } + } + return false; +} + +void ORoadmap::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& _rRect) +{ + if (!m_pImpl->m_bPaintInitialized) + implInit(rRenderContext); + Control::Paint(rRenderContext, _rRect); + + // draw the bitmap + if (!m_pImpl->getPicture().IsEmpty()) + { + Size aBitmapSize = m_pImpl->getPicture().GetSizePixel(); + Size aMySize(GetOutputSizePixel()); + + Point aBitmapPos(aMySize.Width() - aBitmapSize.Width(), aMySize.Height() - aBitmapSize.Height()); + + // draw it + rRenderContext.DrawBitmapEx( aBitmapPos, m_pImpl->getPicture() ); + } + + // draw the headline + DrawHeadline(rRenderContext); +} + +void ORoadmap::DrawHeadline(vcl::RenderContext& rRenderContext) +{ + Point aTextPos = LogicToPixel(Point(ROADMAP_INDENT_X, 8), MapMode(MapUnit::MapAppFont)); + + Size aOutputSize(GetOutputSizePixel()); + + // draw it + rRenderContext.DrawText(tools::Rectangle(aTextPos, aOutputSize), GetText(), + DrawTextFlags::Left | DrawTextFlags::Top | DrawTextFlags::MultiLine | DrawTextFlags::WordBreak); + rRenderContext.DrawTextLine(aTextPos, aOutputSize.Width(), STRIKEOUT_NONE, LINESTYLE_SINGLE, LINESTYLE_NONE); + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + rRenderContext.SetLineColor(rStyleSettings.GetFieldTextColor()); + rRenderContext.SetTextColor(rStyleSettings.GetFieldTextColor()); +} + +RoadmapItem* ORoadmap::GetByPointer(vcl::Window const * pWindow) +{ + const HL_Vector& rItems = m_pImpl->getHyperLabels(); + for (auto const& item : rItems) + { + if ( item->Contains( pWindow ) ) + return item; + } + return nullptr; +} + +bool ORoadmap::PreNotify(NotifyEvent& _rNEvt) +{ + // capture KeyEvents for taskpane cycling + if ( _rNEvt.GetType() == MouseNotifyEvent::KEYINPUT ) + { + vcl::Window* pWindow = _rNEvt.GetWindow(); + RoadmapItem* pItem = GetByPointer( pWindow ); + if ( pItem != nullptr ) + { + sal_Int16 nKeyCode = _rNEvt.GetKeyEvent()->GetKeyCode().GetCode(); + switch( nKeyCode ) + { + case KEY_UP: + { // Note: Performance wise this is not optimal, because we search for an ID in the labels + // and afterwards we search again for a label with the appropriate ID -> + // unnecessarily we search twice!!! + ItemId nPrevItemID = GetPreviousAvailableItemId( pItem->GetIndex() ); + if ( nPrevItemID != -1 ) + return SelectRoadmapItemByID( nPrevItemID ); + } + break; + case KEY_DOWN: + { + ItemId nNextItemID = GetNextAvailableItemId( pItem->GetIndex() ); + if ( nNextItemID != -1 ) + return SelectRoadmapItemByID( nNextItemID ); + } + break; + case KEY_SPACE: + return SelectRoadmapItemByID( pItem->GetID() ); + } + } + } + return Window::PreNotify( _rNEvt ); +} + +IMPL_LINK(ORoadmap, ImplClickHdl, HyperLabel*, CurHyperLabel, void) +{ + SelectRoadmapItemByID( CurHyperLabel->GetID() ); +} + +void ORoadmap::DataChanged(const DataChangedEvent& rDCEvt) +{ + if (!((( rDCEvt.GetType() == DataChangedEventType::SETTINGS ) || + ( rDCEvt.GetType() == DataChangedEventType::DISPLAY )) && + ( rDCEvt.GetFlags() & AllSettingsFlags::STYLE ))) + return; + + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + SetBackground( Wallpaper( rStyleSettings.GetFieldColor() ) ); + Color aTextColor = rStyleSettings.GetFieldTextColor(); + vcl::Font aFont = GetFont(); + aFont.SetColor( aTextColor ); + SetFont( aFont ); + RoadmapTypes::ItemId curItemID = GetCurrentRoadmapItemID(); + RoadmapItem* pLabelItem = GetByID( curItemID ); + if (pLabelItem != nullptr) + { + pLabelItem->ToggleBackgroundColor(rStyleSettings.GetHighlightColor()); + } + Invalidate(); +} + +void ORoadmap::ApplySettings(vcl::RenderContext& rRenderContext) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + Color aTextColor = rStyleSettings.GetFieldTextColor(); + vcl::Font aFont = rRenderContext.GetFont(); + aFont.SetColor(aTextColor); + aFont.SetWeight(WEIGHT_BOLD); + aFont.SetUnderline(LINESTYLE_SINGLE); + rRenderContext.SetFont(aFont); + rRenderContext.SetBackground(rStyleSettings.GetFieldColor()); +} + +RoadmapItem::RoadmapItem(ORoadmap& _rParent, const Size& _rItemPlayground) + : m_aItemPlayground(_rItemPlayground) +{ + mpID = VclPtr<IDLabel>::Create( &_rParent, WB_WORDBREAK ); + mpID->Show(); + mpDescription = VclPtr<HyperLabel>::Create( &_rParent, WB_NOTABSTOP | WB_WORDBREAK ); + mpDescription->Show(); +} + +RoadmapItem::~RoadmapItem() +{ + mpID.disposeAndClear(); + mpDescription.disposeAndClear(); +} + +bool RoadmapItem::Contains(const vcl::Window* _pWindow) const +{ + return ( mpID == _pWindow ) || ( mpDescription == _pWindow ); +} + +void RoadmapItem::GrabFocus() +{ + if ( mpDescription ) + mpDescription->GrabFocus(); +} + +void RoadmapItem::SetInteractive(bool _bInteractive) +{ + if ( mpDescription ) + mpDescription->SetInteractive(_bInteractive); +} + +void RoadmapItem::SetID(sal_Int16 ID) +{ + if ( mpDescription ) + mpDescription->SetID(ID); +} + +sal_Int16 RoadmapItem::GetID() const +{ + return mpDescription ? mpDescription->GetID() : sal_Int16(-1); +} + +void RoadmapItem::ImplUpdateIndex(const ItemIndex _nIndex) +{ + mpDescription->SetIndex( _nIndex ); + + OUString aIDText = OUString::number( _nIndex + 1 ) + "."; + mpID->SetText( aIDText ); + + // update the geometry of both controls + ImplUpdatePosSize(); +} + +void RoadmapItem::SetIndex(ItemIndex Index) +{ + ImplUpdateIndex(Index); +} + +RoadmapTypes::ItemIndex RoadmapItem::GetIndex() const +{ + return mpDescription ? mpDescription->GetIndex() : ItemIndex(-1); +} + +void RoadmapItem::SetPosition(RoadmapItem const * _pOldItem) +{ + Point aIDPos; + if ( _pOldItem == nullptr ) + { + aIDPos = mpID->LogicToPixel(Point(ROADMAP_INDENT_X, ROADMAP_INDENT_Y), MapMode(MapUnit::MapAppFont)); + } + else + { + Size aOldSize = _pOldItem->mpDescription->GetSizePixel(); + + aIDPos = _pOldItem->mpID->GetPosPixel(); + aIDPos.AdjustY(aOldSize.Height() ); + aIDPos.AdjustY(mpID->GetParent()->LogicToPixel( Size( 0, ROADMAP_ITEM_DISTANCE_Y ) ).Height() ); + } + mpID->SetPosPixel( aIDPos ); + + sal_Int32 nDescPos = aIDPos.X() + mpID->GetSizePixel().Width(); + mpDescription->SetPosPixel( Point( nDescPos, aIDPos.Y() ) ); +} + +void RoadmapItem::Enable(bool _bEnable) +{ + mpID->Enable(_bEnable); + mpDescription->Enable(_bEnable); +} + +bool RoadmapItem::IsEnabled() const +{ + return mpID->IsEnabled(); +} + +void RoadmapItem::ToggleBackgroundColor(const Color& _rGBColor) +{ + if (_rGBColor == COL_TRANSPARENT) + mpID->SetControlBackground(); + else + mpID->SetControlBackground( mpID->GetSettings().GetStyleSettings().GetHighlightColor() ); + mpDescription->ToggleBackgroundColor(_rGBColor); +} + +void RoadmapItem::ImplUpdatePosSize() +{ + // calculate widths + tools::Long nIDWidth = mpID->GetTextWidth( mpID->GetText() ); + tools::Long nMaxIDWidth = mpID->GetTextWidth( "100." ); + nIDWidth = ::std::min( nIDWidth, nMaxIDWidth ); + + // check how many space the description would need + Size aDescriptionSize = mpDescription->CalcMinimumSize( m_aItemPlayground.Width() - nIDWidth ); + + // position and size both controls + Size aIDSize( nIDWidth, aDescriptionSize.Height() ); + mpID->SetSizePixel( aIDSize ); + + Point aIDPos = mpID->GetPosPixel(); + mpDescription->SetPosPixel( Point( aIDPos.X() + nIDWidth, aIDPos.Y() ) ); + mpDescription->SetSizePixel( aDescriptionSize ); +} + +void RoadmapItem::Update(ItemIndex RMIndex, const OUString& _rText) +{ + // update description label + mpDescription->SetLabel( _rText ); + + // update the index in both controls, which triggers updating the geometry of both + ImplUpdateIndex( RMIndex ); +} + +void RoadmapItem::SetClickHdl(const Link<HyperLabel*,void>& rLink) +{ + if ( mpDescription ) + mpDescription->SetClickHdl( rLink); +} + +IDLabel::IDLabel(vcl::Window* _pParent, WinBits _nWinStyle) + : FixedText(_pParent, _nWinStyle) +{ +} + +void IDLabel::ApplySettings(vcl::RenderContext& rRenderContext) +{ + FixedText::ApplySettings(rRenderContext); + + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + if (GetControlBackground() == COL_TRANSPARENT) + rRenderContext.SetTextColor(rStyleSettings.GetFieldTextColor()); + else + rRenderContext.SetTextColor(rStyleSettings.GetHighlightTextColor()); +} + +void IDLabel::DataChanged(const DataChangedEvent& rDCEvt) +{ + FixedText::DataChanged( rDCEvt ); + + if ((( rDCEvt.GetType() == DataChangedEventType::SETTINGS ) || + ( rDCEvt.GetType() == DataChangedEventType::DISPLAY )) && + ( rDCEvt.GetFlags() & AllSettingsFlags::STYLE )) + { + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + if (GetControlBackground() != COL_TRANSPARENT) + SetControlBackground(rStyleSettings.GetHighlightColor()); + Invalidate(); + } +} + +} // namespace vcl + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/roadmapwizard.cxx b/vcl/source/control/roadmapwizard.cxx new file mode 100644 index 000000000..b1738d5be --- /dev/null +++ b/vcl/source/control/roadmapwizard.cxx @@ -0,0 +1,798 @@ +/* -*- 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 <vcl/toolkit/roadmap.hxx> +#include <tools/debug.hxx> +#include <osl/diagnose.h> + +#include <strings.hrc> +#include <svdata.hxx> +#include <wizdlg.hxx> + +#include <vector> +#include <map> +#include <set> + +#include "wizimpldata.hxx" +#include <uiobject-internal.hxx> + +namespace vcl +{ + using namespace RoadmapWizardTypes; + + namespace + { + typedef ::std::set< WizardTypes::WizardState > StateSet; + + typedef ::std::map< + PathId, + WizardPath + > Paths; + + typedef ::std::map< + WizardTypes::WizardState, + ::std::pair< + OUString, + RoadmapPageFactory + > + > StateDescriptions; + } + + struct RoadmapWizardImpl + { + ScopedVclPtr<ORoadmap> pRoadmap; + Paths aPaths; + PathId nActivePath; + StateDescriptions aStateDescriptors; + StateSet aDisabledStates; + bool bActivePathIsDefinite; + + RoadmapWizardImpl() + :pRoadmap( nullptr ) + ,nActivePath( -1 ) + ,bActivePathIsDefinite( false ) + { + } + + /// returns the index of the current state in given path, or -1 + static sal_Int32 getStateIndexInPath( WizardTypes::WizardState _nState, const WizardPath& _rPath ); + /// returns the index of the current state in the path with the given id, or -1 + sal_Int32 getStateIndexInPath( WizardTypes::WizardState _nState, PathId _nPathId ); + /// returns the index of the first state in which the two given paths differ + static sal_Int32 getFirstDifferentIndex( const WizardPath& _rLHS, const WizardPath& _rRHS ); + }; + + + sal_Int32 RoadmapWizardImpl::getStateIndexInPath( WizardTypes::WizardState _nState, const WizardPath& _rPath ) + { + sal_Int32 nStateIndexInPath = 0; + bool bFound = false; + for (auto const& path : _rPath) + { + if (path == _nState) + { + bFound = true; + break; + } + ++nStateIndexInPath; + } + if (!bFound) + nStateIndexInPath = -1; + return nStateIndexInPath; + } + + + sal_Int32 RoadmapWizardImpl::getStateIndexInPath( WizardTypes::WizardState _nState, PathId _nPathId ) + { + sal_Int32 nStateIndexInPath = -1; + Paths::const_iterator aPathPos = aPaths.find( _nPathId ); + if ( aPathPos != aPaths.end( ) ) + nStateIndexInPath = getStateIndexInPath( _nState, aPathPos->second ); + return nStateIndexInPath; + } + + + sal_Int32 RoadmapWizardImpl::getFirstDifferentIndex( const WizardPath& _rLHS, const WizardPath& _rRHS ) + { + sal_Int32 nMinLength = ::std::min( _rLHS.size(), _rRHS.size() ); + for ( sal_Int32 nCheck = 0; nCheck < nMinLength; ++nCheck ) + { + if ( _rLHS[ nCheck ] != _rRHS[ nCheck ] ) + return nCheck; + } + return nMinLength; + } + + //= RoadmapWizard + RoadmapWizard::RoadmapWizard(vcl::Window* pParent, WinBits nStyle, InitFlag eFlag) + : Dialog(pParent, nStyle, eFlag) + , maWizardLayoutIdle("vcl RoadmapWizard maWizardLayoutIdle") + , m_pFinish(nullptr) + , m_pCancel(nullptr) + , m_pNextPage(nullptr) + , m_pPrevPage(nullptr) + , m_pHelp(nullptr) + , m_xWizardImpl(new WizardMachineImplData) + , m_xRoadmapImpl(new RoadmapWizardImpl) + { + mpFirstPage = nullptr; + mpFirstBtn = nullptr; + mpCurTabPage = nullptr; + mpPrevBtn = nullptr; + mpNextBtn = nullptr; + mpViewWindow = nullptr; + mnCurLevel = 0; + mbEmptyViewMargin = false; + mnLeftAlignCount = 0; + + maWizardLayoutIdle.SetPriority(TaskPriority::RESIZE); + maWizardLayoutIdle.SetInvokeHandler( LINK( this, RoadmapWizard, ImplHandleWizardLayoutTimerHdl ) ); + + implConstruct(WizardButtonFlags::NEXT | WizardButtonFlags::PREVIOUS | WizardButtonFlags::FINISH | WizardButtonFlags::CANCEL | WizardButtonFlags::HELP); + + SetLeftAlignedButtonCount( 1 ); + mbEmptyViewMargin = true; + + m_xRoadmapImpl->pRoadmap.disposeAndReset( VclPtr<ORoadmap>::Create( this, WB_TABSTOP ) ); + m_xRoadmapImpl->pRoadmap->SetText( VclResId( STR_WIZDLG_ROADMAP_TITLE ) ); + m_xRoadmapImpl->pRoadmap->SetPosPixel( Point( 0, 0 ) ); + m_xRoadmapImpl->pRoadmap->SetItemSelectHdl( LINK( this, RoadmapWizard, OnRoadmapItemSelected ) ); + + Size aRoadmapSize = LogicToPixel(Size(85, 0), MapMode(MapUnit::MapAppFont)); + aRoadmapSize.setHeight( GetSizePixel().Height() ); + m_xRoadmapImpl->pRoadmap->SetSizePixel( aRoadmapSize ); + + mpViewWindow = m_xRoadmapImpl->pRoadmap; + m_xRoadmapImpl->pRoadmap->Show(); + } + + RoadmapWizardMachine::RoadmapWizardMachine(weld::Window* pParent) + : WizardMachine(pParent, WizardButtonFlags::NEXT | WizardButtonFlags::PREVIOUS | WizardButtonFlags::FINISH | WizardButtonFlags::CANCEL | WizardButtonFlags::HELP) + , m_pImpl( new RoadmapWizardImpl ) + { + m_xAssistant->connect_jump_page(LINK(this, RoadmapWizardMachine, OnRoadmapItemSelected)); + } + + void RoadmapWizard::ShowRoadmap(bool bShow) + { + m_xRoadmapImpl->pRoadmap->Show(bShow); + CalcAndSetSize(); + } + + RoadmapWizard::~RoadmapWizard() + { + disposeOnce(); + } + + RoadmapWizardMachine::~RoadmapWizardMachine() + { + } + + void RoadmapWizard::dispose() + { + m_xRoadmapImpl.reset(); + + m_pFinish.disposeAndClear(); + m_pCancel.disposeAndClear(); + m_pNextPage.disposeAndClear(); + m_pPrevPage.disposeAndClear(); + m_pHelp.disposeAndClear(); + + if (m_xWizardImpl) + { + for (WizardTypes::WizardState i = 0; i < m_xWizardImpl->nFirstUnknownPage; ++i) + { + TabPage *pPage = GetPage(i); + if (pPage) + pPage->disposeOnce(); + } + m_xWizardImpl.reset(); + } + + maWizardLayoutIdle.Stop(); + + // Remove all buttons + while ( mpFirstBtn ) + RemoveButton( mpFirstBtn->mpButton ); + + // Remove all pages + while ( mpFirstPage ) + RemovePage( mpFirstPage->mpPage ); + + mpCurTabPage.clear(); + mpPrevBtn.clear(); + mpNextBtn.clear(); + mpViewWindow.clear(); + Dialog::dispose(); + } + + void RoadmapWizard::SetRoadmapHelpId( const OString& _rId ) + { + m_xRoadmapImpl->pRoadmap->SetHelpId( _rId ); + } + + void RoadmapWizardMachine::SetRoadmapHelpId(const OString& rId) + { + m_xAssistant->set_page_side_help_id(rId); + } + + void RoadmapWizardMachine::declarePath( PathId _nPathId, const WizardPath& _lWizardStates) + { + m_pImpl->aPaths.emplace( _nPathId, _lWizardStates ); + + if ( m_pImpl->aPaths.size() == 1 ) + // the very first path -> activate it + activatePath( _nPathId ); + else + implUpdateRoadmap( ); + } + + void RoadmapWizardMachine::activatePath( PathId _nPathId, bool _bDecideForIt ) + { + if ( ( _nPathId == m_pImpl->nActivePath ) && ( _bDecideForIt == m_pImpl->bActivePathIsDefinite ) ) + // nothing to do + return; + + // does the given path exist? + Paths::const_iterator aNewPathPos = m_pImpl->aPaths.find( _nPathId ); + DBG_ASSERT( aNewPathPos != m_pImpl->aPaths.end(), "RoadmapWizard::activate: there is no such path!" ); + if ( aNewPathPos == m_pImpl->aPaths.end() ) + return; + + // determine the index of the current state in the current path + sal_Int32 nCurrentStatePathIndex = -1; + if ( m_pImpl->nActivePath != -1 ) + nCurrentStatePathIndex = m_pImpl->getStateIndexInPath( getCurrentState(), m_pImpl->nActivePath ); + + DBG_ASSERT( static_cast<sal_Int32>(aNewPathPos->second.size()) > nCurrentStatePathIndex, + "RoadmapWizard::activate: you cannot activate a path which has less states than we've already advanced!" ); + // If this asserts, this for instance means that we are already in state number, say, 5 + // of our current path, and the caller tries to activate a path which has less than 5 + // states + if ( static_cast<sal_Int32>(aNewPathPos->second.size()) <= nCurrentStatePathIndex ) + return; + + // assert that the current and the new path are equal, up to nCurrentStatePathIndex + Paths::const_iterator aActivePathPos = m_pImpl->aPaths.find( m_pImpl->nActivePath ); + if ( aActivePathPos != m_pImpl->aPaths.end() ) + { + if ( RoadmapWizardImpl::getFirstDifferentIndex( aActivePathPos->second, aNewPathPos->second ) <= nCurrentStatePathIndex ) + { + OSL_FAIL( "RoadmapWizard::activate: you cannot activate a path which conflicts with the current one *before* the current state!" ); + return; + } + } + + m_pImpl->nActivePath = _nPathId; + m_pImpl->bActivePathIsDefinite = _bDecideForIt; + + implUpdateRoadmap( ); + } + + void RoadmapWizard::implUpdateRoadmap( ) + { + DBG_ASSERT( m_xRoadmapImpl->aPaths.find( m_xRoadmapImpl->nActivePath ) != m_xRoadmapImpl->aPaths.end(), + "RoadmapWizard::implUpdateRoadmap: there is no such path!" ); + const WizardPath& rActivePath( m_xRoadmapImpl->aPaths[ m_xRoadmapImpl->nActivePath ] ); + + sal_Int32 nCurrentStatePathIndex = RoadmapWizardImpl::getStateIndexInPath( getCurrentState(), rActivePath ); + if (nCurrentStatePathIndex < 0) + return; + + // determine up to which index (in the new path) we have to display the items + RoadmapTypes::ItemIndex nUpperStepBoundary = static_cast<RoadmapTypes::ItemIndex>(rActivePath.size()); + bool bIncompletePath = false; + if ( !m_xRoadmapImpl->bActivePathIsDefinite ) + { + for (auto const& path : m_xRoadmapImpl->aPaths) + { + if ( path.first == m_xRoadmapImpl->nActivePath ) + // it's the path we are just activating -> no need to check anything + continue; + // the index from which on both paths differ + sal_Int32 nDivergenceIndex = RoadmapWizardImpl::getFirstDifferentIndex( rActivePath, path.second ); + if ( nDivergenceIndex <= nCurrentStatePathIndex ) + // they differ in an index which we have already left behind us + // -> this is no conflict anymore + continue; + + // the path conflicts with our new path -> don't activate the + // *complete* new path, but only up to the step which is unambiguous + nUpperStepBoundary = nDivergenceIndex; + bIncompletePath = true; + } + } + + // now, we have to remove all items after nCurrentStatePathIndex, and insert the items from the active + // path, up to (excluding) nUpperStepBoundary + RoadmapTypes::ItemIndex nLoopUntil = ::std::max( nUpperStepBoundary, m_xRoadmapImpl->pRoadmap->GetItemCount() ); + for ( RoadmapTypes::ItemIndex nItemIndex = nCurrentStatePathIndex; nItemIndex < nLoopUntil; ++nItemIndex ) + { + bool bExistentItem = ( nItemIndex < m_xRoadmapImpl->pRoadmap->GetItemCount() ); + bool bNeedItem = ( nItemIndex < nUpperStepBoundary ); + + bool bInsertItem = false; + if ( bExistentItem ) + { + if ( !bNeedItem ) + { + while ( nItemIndex < m_xRoadmapImpl->pRoadmap->GetItemCount() ) + m_xRoadmapImpl->pRoadmap->DeleteRoadmapItem( nItemIndex ); + break; + } + else + { + // there is an item with this index in the roadmap - does it match what is requested by + // the respective state in the active path? + RoadmapTypes::ItemId nPresentItemId = m_xRoadmapImpl->pRoadmap->GetItemID( nItemIndex ); + WizardTypes::WizardState nRequiredState = rActivePath[ nItemIndex ]; + if ( nPresentItemId != nRequiredState ) + { + m_xRoadmapImpl->pRoadmap->DeleteRoadmapItem( nItemIndex ); + bInsertItem = true; + } + } + } + else + { + DBG_ASSERT( bNeedItem, "RoadmapWizard::implUpdateRoadmap: ehm - none needed, none present - why did the loop not terminate?" ); + bInsertItem = bNeedItem; + } + + WizardTypes::WizardState nState( rActivePath[ nItemIndex ] ); + if ( bInsertItem ) + { + m_xRoadmapImpl->pRoadmap->InsertRoadmapItem( + nItemIndex, + getStateDisplayName( nState ), + nState, + true + ); + } + + const bool bEnable = m_xRoadmapImpl->aDisabledStates.find( nState ) == m_xRoadmapImpl->aDisabledStates.end(); + m_xRoadmapImpl->pRoadmap->EnableRoadmapItem( m_xRoadmapImpl->pRoadmap->GetItemID( nItemIndex ), bEnable ); + } + + m_xRoadmapImpl->pRoadmap->SetRoadmapComplete( !bIncompletePath ); + } + + void RoadmapWizardMachine::implUpdateRoadmap( ) + { + + DBG_ASSERT( m_pImpl->aPaths.find( m_pImpl->nActivePath ) != m_pImpl->aPaths.end(), + "RoadmapWizard::implUpdateRoadmap: there is no such path!" ); + const WizardPath& rActivePath( m_pImpl->aPaths[ m_pImpl->nActivePath ] ); + + sal_Int32 nCurrentStatePathIndex = RoadmapWizardImpl::getStateIndexInPath( getCurrentState(), rActivePath ); + if (nCurrentStatePathIndex < 0) + return; + + // determine up to which index (in the new path) we have to display the items + RoadmapTypes::ItemIndex nUpperStepBoundary = static_cast<RoadmapTypes::ItemIndex>(rActivePath.size()); + if ( !m_pImpl->bActivePathIsDefinite ) + { + for (auto const& path : m_pImpl->aPaths) + { + if ( path.first == m_pImpl->nActivePath ) + // it's the path we are just activating -> no need to check anything + continue; + // the index from which on both paths differ + sal_Int32 nDivergenceIndex = RoadmapWizardImpl::getFirstDifferentIndex( rActivePath, path.second ); + if ( nDivergenceIndex <= nCurrentStatePathIndex ) + // they differ in an index which we have already left behind us + // -> this is no conflict anymore + continue; + + // the path conflicts with our new path -> don't activate the + // *complete* new path, but only up to the step which is unambiguous + nUpperStepBoundary = nDivergenceIndex; + } + } + + // can we advance from the current page? + bool bCurrentPageCanAdvance = true; + BuilderPage* pCurrentPage = GetPage( getCurrentState() ); + if ( pCurrentPage ) + { + const IWizardPageController* pController = getPageController( GetPage( getCurrentState() ) ); + OSL_ENSURE( pController != nullptr, "RoadmapWizard::implUpdateRoadmap: no controller for the current page!" ); + bCurrentPageCanAdvance = !pController || pController->canAdvance(); + } + + // now, we have to remove all items after nCurrentStatePathIndex, and insert the items from the active + // path, up to (excluding) nUpperStepBoundary + RoadmapTypes::ItemIndex nRoadmapItems = m_xAssistant->get_n_pages(); + RoadmapTypes::ItemIndex nLoopUntil = ::std::max( nUpperStepBoundary, nRoadmapItems ); + for ( RoadmapTypes::ItemIndex nItemIndex = nCurrentStatePathIndex; nItemIndex < nLoopUntil; ++nItemIndex ) + { + bool bExistentItem = ( nItemIndex < nRoadmapItems ); + bool bNeedItem = ( nItemIndex < nUpperStepBoundary ); + + bool bInsertItem = false; + if ( bExistentItem ) + { + if ( !bNeedItem ) + { + int nPages = nRoadmapItems; + for (int i = nPages - 1; i >= nItemIndex; --i) + { + m_xAssistant->set_page_title(m_xAssistant->get_page_ident(i), ""); + --nRoadmapItems; + } + break; + } + else + { + // there is an item with this index in the roadmap - does it match what is requested by + // the respective state in the active path? + RoadmapTypes::ItemId nPresentItemId = m_xAssistant->get_page_ident(nItemIndex).toInt32(); + WizardTypes::WizardState nRequiredState = rActivePath[ nItemIndex ]; + if ( nPresentItemId != nRequiredState ) + { + m_xAssistant->set_page_title(OString::number(nPresentItemId), ""); + bInsertItem = true; + } + } + } + else + { + DBG_ASSERT( bNeedItem, "RoadmapWizard::implUpdateRoadmap: ehm - none needed, none present - why did the loop not terminate?" ); + bInsertItem = bNeedItem; + } + + WizardTypes::WizardState nState( rActivePath[ nItemIndex ] ); + + if ( bInsertItem ) + { + GetOrCreatePage(nState); + } + + OString sIdent(OString::number(nState)); + m_xAssistant->set_page_index(sIdent, nItemIndex); + m_xAssistant->set_page_title(sIdent, getStateDisplayName(nState)); + + // if the item is *after* the current state, but the current page does not + // allow advancing, the disable the state. This relieves derived classes + // from disabling all future states just because the current state does not + // (yet) allow advancing. + const bool bUnconditionedDisable = !bCurrentPageCanAdvance && ( nItemIndex > nCurrentStatePathIndex ); + const bool bEnable = !bUnconditionedDisable && ( m_pImpl->aDisabledStates.find( nState ) == m_pImpl->aDisabledStates.end() ); + m_xAssistant->set_page_sensitive(sIdent, bEnable); + } + } + + WizardTypes::WizardState RoadmapWizard::determineNextState( WizardTypes::WizardState _nCurrentState ) const + { + sal_Int32 nCurrentStatePathIndex = -1; + + Paths::const_iterator aActivePathPos = m_xRoadmapImpl->aPaths.find( m_xRoadmapImpl->nActivePath ); + if ( aActivePathPos != m_xRoadmapImpl->aPaths.end() ) + nCurrentStatePathIndex = RoadmapWizardImpl::getStateIndexInPath( _nCurrentState, aActivePathPos->second ); + + DBG_ASSERT( nCurrentStatePathIndex != -1, "RoadmapWizard::determineNextState: ehm - how can we travel if there is no (valid) active path?" ); + if ( nCurrentStatePathIndex == -1 ) + return WZS_INVALID_STATE; + + sal_Int32 nNextStateIndex = nCurrentStatePathIndex + 1; + + while ( ( nNextStateIndex < static_cast<sal_Int32>(aActivePathPos->second.size()) ) + && ( m_xRoadmapImpl->aDisabledStates.find( aActivePathPos->second[ nNextStateIndex ] ) != m_xRoadmapImpl->aDisabledStates.end() ) + ) + { + ++nNextStateIndex; + } + + if ( nNextStateIndex >= static_cast<sal_Int32>(aActivePathPos->second.size()) ) + // there is no next state in the current path (at least none which is enabled) + return WZS_INVALID_STATE; + + return aActivePathPos->second[ nNextStateIndex ]; + } + + WizardTypes::WizardState RoadmapWizardMachine::determineNextState( WizardTypes::WizardState _nCurrentState ) const + { + sal_Int32 nCurrentStatePathIndex = -1; + + Paths::const_iterator aActivePathPos = m_pImpl->aPaths.find( m_pImpl->nActivePath ); + if ( aActivePathPos != m_pImpl->aPaths.end() ) + nCurrentStatePathIndex = RoadmapWizardImpl::getStateIndexInPath( _nCurrentState, aActivePathPos->second ); + + DBG_ASSERT( nCurrentStatePathIndex != -1, "RoadmapWizard::determineNextState: ehm - how can we travel if there is no (valid) active path?" ); + if ( nCurrentStatePathIndex == -1 ) + return WZS_INVALID_STATE; + + sal_Int32 nNextStateIndex = nCurrentStatePathIndex + 1; + + while ( ( nNextStateIndex < static_cast<sal_Int32>(aActivePathPos->second.size()) ) + && ( m_pImpl->aDisabledStates.find( aActivePathPos->second[ nNextStateIndex ] ) != m_pImpl->aDisabledStates.end() ) + ) + { + ++nNextStateIndex; + } + + if ( nNextStateIndex >= static_cast<sal_Int32>(aActivePathPos->second.size()) ) + // there is no next state in the current path (at least none which is enabled) + return WZS_INVALID_STATE; + + return aActivePathPos->second[ nNextStateIndex ]; + } + + bool RoadmapWizardMachine::canAdvance() const + { + if ( !m_pImpl->bActivePathIsDefinite ) + { + // check how many paths are still allowed + const WizardPath& rActivePath( m_pImpl->aPaths[ m_pImpl->nActivePath ] ); + + // if current path has only the base item, it is not possible to proceed without activating another path + if(rActivePath.size()<=1) + return false; + + sal_Int32 nCurrentStatePathIndex = RoadmapWizardImpl::getStateIndexInPath( getCurrentState(), rActivePath ); + + size_t nPossiblePaths(0); + for (auto const& path : m_pImpl->aPaths) + { + // the index from which on both paths differ + sal_Int32 nDivergenceIndex = RoadmapWizardImpl::getFirstDifferentIndex( rActivePath, path.second ); + + if ( nDivergenceIndex > nCurrentStatePathIndex ) + // this path is still a possible path + nPossiblePaths += 1; + } + + // if we have more than one path which is still possible, then we assume + // to always have a next state. Though there might be scenarios where this + // is not true, but this is too sophisticated (means not really needed) right now. + if ( nPossiblePaths > 1 ) + return true; + } + + const WizardPath& rPath = m_pImpl->aPaths[ m_pImpl->nActivePath ]; + return *rPath.rbegin() != getCurrentState(); + } + + void RoadmapWizardMachine::updateTravelUI() + { + WizardMachine::updateTravelUI(); + + // disable the "Previous" button if all states in our history are disabled + std::vector< WizardTypes::WizardState > aHistory; + getStateHistory( aHistory ); + bool bHaveEnabledState = false; + for (auto const& state : aHistory) + { + if ( isStateEnabled(state) ) + { + bHaveEnabledState = true; + break; + } + } + + enableButtons( WizardButtonFlags::PREVIOUS, bHaveEnabledState ); + + implUpdateRoadmap(); + } + + IMPL_LINK_NOARG(RoadmapWizard, OnRoadmapItemSelected, LinkParamNone*, void) + { + RoadmapTypes::ItemId nCurItemId = m_xRoadmapImpl->pRoadmap->GetCurrentRoadmapItemID(); + if ( nCurItemId == getCurrentState() ) + // nothing to do + return; + + if ( isTravelingSuspended() ) + return; + + RoadmapWizardTravelSuspension aTravelGuard( *this ); + + sal_Int32 nCurrentIndex = m_xRoadmapImpl->getStateIndexInPath( getCurrentState(), m_xRoadmapImpl->nActivePath ); + sal_Int32 nNewIndex = m_xRoadmapImpl->getStateIndexInPath( nCurItemId, m_xRoadmapImpl->nActivePath ); + + DBG_ASSERT( ( nCurrentIndex != -1 ) && ( nNewIndex != -1 ), + "RoadmapWizard::OnRoadmapItemSelected: something's wrong here!" ); + if ( ( nCurrentIndex == -1 ) || ( nNewIndex == -1 ) ) + { + return; + } + + bool bResult = true; + if ( nNewIndex > nCurrentIndex ) + { + bResult = skipUntil( static_cast<WizardTypes::WizardState>(nCurItemId) ); + WizardTypes::WizardState nTemp = static_cast<WizardTypes::WizardState>(nCurItemId); + while( nTemp ) + { + if( m_xRoadmapImpl->aDisabledStates.find( --nTemp ) != m_xRoadmapImpl->aDisabledStates.end() ) + removePageFromHistory( nTemp ); + } + } + else + bResult = skipBackwardUntil( static_cast<WizardTypes::WizardState>(nCurItemId) ); + + if ( !bResult ) + m_xRoadmapImpl->pRoadmap->SelectRoadmapItemByID( getCurrentState() ); + } + + IMPL_LINK(RoadmapWizardMachine, OnRoadmapItemSelected, const OString&, rCurItemId, bool) + { + int nCurItemId = rCurItemId.toInt32(); + + if ( nCurItemId == getCurrentState() ) + // nothing to do + return false; + + if ( isTravelingSuspended() ) + return false; + + WizardTravelSuspension aTravelGuard( *this ); + + sal_Int32 nCurrentIndex = m_pImpl->getStateIndexInPath( getCurrentState(), m_pImpl->nActivePath ); + sal_Int32 nNewIndex = m_pImpl->getStateIndexInPath( nCurItemId, m_pImpl->nActivePath ); + + DBG_ASSERT( ( nCurrentIndex != -1 ) && ( nNewIndex != -1 ), + "RoadmapWizard::OnRoadmapItemSelected: something's wrong here!" ); + if ( ( nCurrentIndex == -1 ) || ( nNewIndex == -1 ) ) + { + return false; + } + + bool bResult = true; + if ( nNewIndex > nCurrentIndex ) + { + bResult = skipUntil( static_cast<WizardTypes::WizardState>(nCurItemId) ); + WizardTypes::WizardState nTemp = static_cast<WizardTypes::WizardState>(nCurItemId); + while( nTemp ) + { + if( m_pImpl->aDisabledStates.find( --nTemp ) != m_pImpl->aDisabledStates.end() ) + removePageFromHistory( nTemp ); + } + } + else + bResult = skipBackwardUntil( static_cast<WizardTypes::WizardState>(nCurItemId) ); + + return bResult; + } + + void RoadmapWizard::enterState(WizardTypes::WizardState /*nState*/) + { + // synchronize the roadmap + implUpdateRoadmap( ); + m_xRoadmapImpl->pRoadmap->SelectRoadmapItemByID( getCurrentState() ); + } + + void RoadmapWizardMachine::enterState( WizardTypes::WizardState _nState ) + { + WizardMachine::enterState( _nState ); + + // synchronize the roadmap + implUpdateRoadmap(); + } + + OUString RoadmapWizard::getStateDisplayName( WizardTypes::WizardState _nState ) const + { + OUString sDisplayName; + + StateDescriptions::const_iterator pos = m_xRoadmapImpl->aStateDescriptors.find( _nState ); + OSL_ENSURE( pos != m_xRoadmapImpl->aStateDescriptors.end(), + "RoadmapWizard::getStateDisplayName: no default implementation available for this state!" ); + if ( pos != m_xRoadmapImpl->aStateDescriptors.end() ) + sDisplayName = pos->second.first; + + return sDisplayName; + } + + OUString RoadmapWizardMachine::getStateDisplayName( WizardTypes::WizardState _nState ) const + { + OUString sDisplayName; + + StateDescriptions::const_iterator pos = m_pImpl->aStateDescriptors.find( _nState ); + OSL_ENSURE( pos != m_pImpl->aStateDescriptors.end(), + "RoadmapWizard::getStateDisplayName: no default implementation available for this state!" ); + if ( pos != m_pImpl->aStateDescriptors.end() ) + sDisplayName = pos->second.first; + + return sDisplayName; + } + + VclPtr<TabPage> RoadmapWizard::createPage( WizardTypes::WizardState _nState ) + { + VclPtr<TabPage> pPage; + + StateDescriptions::const_iterator pos = m_xRoadmapImpl->aStateDescriptors.find( _nState ); + OSL_ENSURE( pos != m_xRoadmapImpl->aStateDescriptors.end(), + "RoadmapWizard::createPage: no default implementation available for this state!" ); + if ( pos != m_xRoadmapImpl->aStateDescriptors.end() ) + { + RoadmapPageFactory pFactory = pos->second.second; + pPage = (*pFactory)( *this ); + } + + return pPage; + } + + void RoadmapWizardMachine::enableState( WizardTypes::WizardState _nState, bool _bEnable ) + { + // remember this (in case the state appears in the roadmap later on) + if ( _bEnable ) + m_pImpl->aDisabledStates.erase( _nState ); + else + { + m_pImpl->aDisabledStates.insert( _nState ); + removePageFromHistory( _nState ); + } + + // if the state is currently in the roadmap, reflect it's new status + m_xAssistant->set_page_sensitive(OString::number(_nState), _bEnable); + } + + bool RoadmapWizardMachine::knowsState( WizardTypes::WizardState i_nState ) const + { + for (auto const& path : m_pImpl->aPaths) + { + for (auto const& state : path.second) + { + if ( state == i_nState ) + return true; + } + } + return false; + } + + bool RoadmapWizardMachine::isStateEnabled( WizardTypes::WizardState _nState ) const + { + return m_pImpl->aDisabledStates.find( _nState ) == m_pImpl->aDisabledStates.end(); + } + + void RoadmapWizard::InsertRoadmapItem(int nItemIndex, const OUString& rText, int nItemId, bool bEnable) + { + m_xRoadmapImpl->pRoadmap->InsertRoadmapItem(nItemIndex, rText, nItemId, bEnable); + } + + void RoadmapWizard::SelectRoadmapItemByID(int nItemId, bool bGrabFocus) + { + m_xRoadmapImpl->pRoadmap->SelectRoadmapItemByID(nItemId, bGrabFocus); + } + + void RoadmapWizard::DeleteRoadmapItems() + { + while (m_xRoadmapImpl->pRoadmap->GetItemCount()) + m_xRoadmapImpl->pRoadmap->DeleteRoadmapItem(0); + } + + void RoadmapWizard::SetItemSelectHdl( const Link<LinkParamNone*,void>& _rHdl ) + { + m_xRoadmapImpl->pRoadmap->SetItemSelectHdl(_rHdl); + } + + int RoadmapWizard::GetCurrentRoadmapItemID() const + { + return m_xRoadmapImpl->pRoadmap->GetCurrentRoadmapItemID(); + } + + FactoryFunction RoadmapWizard::GetUITestFactory() const + { + return RoadmapWizardUIObject::create; + } + +} // namespace vcl + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/scrbar.cxx b/vcl/source/control/scrbar.cxx new file mode 100644 index 000000000..844f80b02 --- /dev/null +++ b/vcl/source/control/scrbar.cxx @@ -0,0 +1,1467 @@ +/* -*- 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 <vcl/event.hxx> +#include <vcl/decoview.hxx> +#include <vcl/scrbar.hxx> +#include <vcl/timer.hxx> +#include <vcl/settings.hxx> +#include <vcl/vclevent.hxx> + +#include <sal/log.hxx> + +/* #i77549# + HACK: for scrollbars in case of thumb rect, page up and page down rect we + abuse the HitTestNativeScrollbar interface. All theming engines but macOS + are actually able to draw the thumb according to our internal representation. + However macOS draws a little outside. The canonical way would be to enhance the + HitTestNativeScrollbar passing a ScrollbarValue additionally so all necessary + information is available in the call. + . + However since there is only this one small exception we will deviate a little and + instead pass the respective rect as control region to allow for a small correction. + + So all places using HitTestNativeScrollbar on ControlPart::ThumbHorz, ControlPart::ThumbVert, + ControlPart::TrackHorzLeft, ControlPart::TrackHorzRight, ControlPart::TrackVertUpper, ControlPart::TrackVertLower + do not use the control rectangle as region but the actual part rectangle, making + only small deviations feasible. +*/ + +#include "thumbpos.hxx" + +#define SCRBAR_DRAW_BTN1 (sal_uInt16(0x0001)) +#define SCRBAR_DRAW_BTN2 (sal_uInt16(0x0002)) +#define SCRBAR_DRAW_PAGE1 (sal_uInt16(0x0004)) +#define SCRBAR_DRAW_PAGE2 (sal_uInt16(0x0008)) +#define SCRBAR_DRAW_THUMB (sal_uInt16(0x0010)) +#define SCRBAR_DRAW_BACKGROUND (sal_uInt16(0x0020)) + +#define SCRBAR_STATE_BTN1_DOWN (sal_uInt16(0x0001)) +#define SCRBAR_STATE_BTN1_DISABLE (sal_uInt16(0x0002)) +#define SCRBAR_STATE_BTN2_DOWN (sal_uInt16(0x0004)) +#define SCRBAR_STATE_BTN2_DISABLE (sal_uInt16(0x0008)) +#define SCRBAR_STATE_PAGE1_DOWN (sal_uInt16(0x0010)) +#define SCRBAR_STATE_PAGE2_DOWN (sal_uInt16(0x0020)) +#define SCRBAR_STATE_THUMB_DOWN (sal_uInt16(0x0040)) + +#define SCRBAR_VIEW_STYLE (WB_3DLOOK | WB_HORZ | WB_VERT) + +struct ImplScrollBarData +{ + AutoTimer maTimer { "vcl::ScrollBar mpData->maTimer" }; + bool mbHide; +}; + +void ScrollBar::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + mpData = nullptr; + mnThumbPixRange = 0; + mnThumbPixPos = 0; + mnThumbPixSize = 0; + mnMinRange = 0; + mnMaxRange = 100; + mnThumbPos = 0; + mnVisibleSize = 0; + mnLineSize = 1; + mnPageSize = 1; + mnDelta = 0; + mnStateFlags = 0; + meScrollType = ScrollType::DontKnow; + mbCalcSize = true; + mbFullDrag = false; + + ImplInitStyle( nStyle ); + Control::ImplInit( pParent, nStyle, nullptr ); + + tools::Long nScrollSize = GetSettings().GetStyleSettings().GetScrollBarSize(); + SetSizePixel( Size( nScrollSize, nScrollSize ) ); +} + +void ScrollBar::ImplInitStyle( WinBits nStyle ) +{ + if ( nStyle & WB_DRAG ) + mbFullDrag = true; + else + mbFullDrag = bool(GetSettings().GetStyleSettings().GetDragFullOptions() & DragFullOptions::Scroll); +} + +ScrollBar::ScrollBar( vcl::Window* pParent, WinBits nStyle ) : + Control( WindowType::SCROLLBAR ) +{ + ImplInit( pParent, nStyle ); +} + +ScrollBar::~ScrollBar() +{ + disposeOnce(); +} + +void ScrollBar::dispose() +{ + mpData.reset(); + Control::dispose(); +} + +void ScrollBar::ImplUpdateRects( bool bUpdate ) +{ + mnStateFlags &= ~SCRBAR_STATE_BTN1_DISABLE; + mnStateFlags &= ~SCRBAR_STATE_BTN2_DISABLE; + + if ( mnThumbPixRange ) + { + if ( GetStyle() & WB_HORZ ) + { + maThumbRect.SetLeft( maTrackRect.Left()+mnThumbPixPos ); + maThumbRect.SetRight( maThumbRect.Left()+mnThumbPixSize-1 ); + if ( !mnThumbPixPos ) + maPage1Rect.SetWidthEmpty(); + else + maPage1Rect.SetRight( maThumbRect.Left()-1 ); + if ( mnThumbPixPos >= (mnThumbPixRange-mnThumbPixSize) ) + maPage2Rect.SetWidthEmpty(); + else + { + maPage2Rect.SetLeft( maThumbRect.Right()+1 ); + maPage2Rect.SetRight( maTrackRect.Right() ); + } + } + else + { + maThumbRect.SetTop( maTrackRect.Top()+mnThumbPixPos ); + maThumbRect.SetBottom( maThumbRect.Top()+mnThumbPixSize-1 ); + if ( !mnThumbPixPos ) + maPage1Rect.SetHeightEmpty(); + else + maPage1Rect.SetBottom( maThumbRect.Top()-1 ); + if ( mnThumbPixPos >= (mnThumbPixRange-mnThumbPixSize) ) + maPage2Rect.SetHeightEmpty(); + else + { + maPage2Rect.SetTop( maThumbRect.Bottom()+1 ); + maPage2Rect.SetBottom( maTrackRect.Bottom() ); + } + } + } + else + { + if ( GetStyle() & WB_HORZ ) + { + const tools::Long nSpace = maTrackRect.Right() - maTrackRect.Left(); + if ( nSpace > 0 ) + { + maPage1Rect.SetLeft( maTrackRect.Left() ); + maPage1Rect.SetRight( maTrackRect.Left() + (nSpace/2) ); + maPage2Rect.SetLeft( maPage1Rect.Right() + 1 ); + maPage2Rect.SetRight( maTrackRect.Right() ); + } + } + else + { + const tools::Long nSpace = maTrackRect.Bottom() - maTrackRect.Top(); + if ( nSpace > 0 ) + { + maPage1Rect.SetTop( maTrackRect.Top() ); + maPage1Rect.SetBottom( maTrackRect.Top() + (nSpace/2) ); + maPage2Rect.SetTop( maPage1Rect.Bottom() + 1 ); + maPage2Rect.SetBottom( maTrackRect.Bottom() ); + } + } + } + + if( !IsNativeControlSupported(ControlType::Scrollbar, ControlPart::Entire) ) + { + // disable scrollbar buttons only in VCL's own 'theme' + // as it is uncommon on other platforms + if ( mnThumbPos == mnMinRange ) + mnStateFlags |= SCRBAR_STATE_BTN1_DISABLE; + if ( mnThumbPos >= (mnMaxRange-mnVisibleSize) ) + mnStateFlags |= SCRBAR_STATE_BTN2_DISABLE; + } + + if ( bUpdate ) + { + Invalidate(); + } +} + +tools::Long ScrollBar::ImplCalcThumbPos( tools::Long nPixPos ) const +{ + // Calculate position + tools::Long nCalcThumbPos; + nCalcThumbPos = ImplMulDiv( nPixPos, mnMaxRange-mnVisibleSize-mnMinRange, + mnThumbPixRange-mnThumbPixSize ); + nCalcThumbPos += mnMinRange; + return nCalcThumbPos; +} + +tools::Long ScrollBar::ImplCalcThumbPosPix( tools::Long nPos ) const +{ + tools::Long nCalcThumbPos; + + // Calculate position + nCalcThumbPos = ImplMulDiv( nPos-mnMinRange, mnThumbPixRange-mnThumbPixSize, + mnMaxRange-mnVisibleSize-mnMinRange ); + + // At the start and end of the ScrollBar, we try to show the display correctly + if ( !nCalcThumbPos && (mnThumbPos > mnMinRange) ) + nCalcThumbPos = 1; + if ( nCalcThumbPos && + ((nCalcThumbPos+mnThumbPixSize) >= mnThumbPixRange) && + (mnThumbPos < (mnMaxRange-mnVisibleSize)) ) + nCalcThumbPos--; + + return nCalcThumbPos; +} + +void ScrollBar::ImplCalc( bool bUpdate ) +{ + const Size aSize = GetOutputSizePixel(); + const tools::Long nMinThumbSize = GetSettings().GetStyleSettings().GetMinThumbSize(); + + if ( mbCalcSize ) + { + Size aOldSize = getCurrentCalcSize(); + + const tools::Rectangle aControlRegion( Point(0,0), aSize ); + tools::Rectangle aBtn1Region, aBtn2Region, aTrackRegion, aBoundingRegion; + + // reset rectangles to empty *and* (0,0) position + maThumbRect = tools::Rectangle(); + maPage1Rect = tools::Rectangle(); + maPage2Rect = tools::Rectangle(); + + if ( GetStyle() & WB_HORZ ) + { + if ( GetNativeControlRegion( ControlType::Scrollbar, IsRTLEnabled()? ControlPart::ButtonRight: ControlPart::ButtonLeft, + aControlRegion, ControlState::NONE, ImplControlValue(), aBoundingRegion, aBtn1Region ) && + GetNativeControlRegion( ControlType::Scrollbar, IsRTLEnabled()? ControlPart::ButtonLeft: ControlPart::ButtonRight, + aControlRegion, ControlState::NONE, ImplControlValue(), aBoundingRegion, aBtn2Region ) ) + { + maBtn1Rect = aBtn1Region; + maBtn2Rect = aBtn2Region; + } + else + { + Size aBtnSize( aSize.Height(), aSize.Height() ); + maBtn2Rect.SetTop( maBtn1Rect.Top() ); + maBtn2Rect.SetLeft( aSize.Width()-aSize.Height() ); + maBtn1Rect.SetSize( aBtnSize ); + maBtn2Rect.SetSize( aBtnSize ); + } + + if ( GetNativeControlRegion( ControlType::Scrollbar, ControlPart::TrackHorzArea, + aControlRegion, ControlState::NONE, ImplControlValue(), aBoundingRegion, aTrackRegion ) ) + maTrackRect = aTrackRegion; + else + maTrackRect = tools::Rectangle::Justify( maBtn1Rect.TopRight(), maBtn2Rect.BottomLeft() ); + + // Check if available space is big enough for thumb ( min thumb size = ScrBar width/height ) + mnThumbPixRange = maTrackRect.Right() - maTrackRect.Left(); + if( mnThumbPixRange > 0 ) + { + maPage1Rect.SetLeft( maTrackRect.Left() ); + maPage1Rect.SetBottom( maTrackRect.Bottom() ); + maPage2Rect.SetBottom (maTrackRect.Bottom() ); + maThumbRect.SetBottom( maTrackRect.Bottom() ); + } + else + mnThumbPixRange = 0; + } + else // WB_VERT + { + if ( GetNativeControlRegion( ControlType::Scrollbar, ControlPart::ButtonUp, + aControlRegion, ControlState::NONE, ImplControlValue(), aBoundingRegion, aBtn1Region ) && + GetNativeControlRegion( ControlType::Scrollbar, ControlPart::ButtonDown, + aControlRegion, ControlState::NONE, ImplControlValue(), aBoundingRegion, aBtn2Region ) ) + { + maBtn1Rect = aBtn1Region; + maBtn2Rect = aBtn2Region; + } + else + { + const Size aBtnSize( aSize.Width(), aSize.Width() ); + maBtn2Rect.SetLeft( maBtn1Rect.Left() ); + maBtn2Rect.SetTop( aSize.Height()-aSize.Width() ); + maBtn1Rect.SetSize( aBtnSize ); + maBtn2Rect.SetSize( aBtnSize ); + } + + if ( GetNativeControlRegion( ControlType::Scrollbar, ControlPart::TrackVertArea, + aControlRegion, ControlState::NONE, ImplControlValue(), aBoundingRegion, aTrackRegion ) ) + maTrackRect = aTrackRegion; + else + maTrackRect = tools::Rectangle::Justify( maBtn1Rect.BottomLeft()+Point(0,1), maBtn2Rect.TopRight() ); + + // Check if available space is big enough for thumb + mnThumbPixRange = maTrackRect.Bottom() - maTrackRect.Top(); + if( mnThumbPixRange > 0 ) + { + maPage1Rect.SetTop( maTrackRect.Top() ); + maPage1Rect.SetRight( maTrackRect.Right() ); + maPage2Rect.SetRight( maTrackRect.Right() ); + maThumbRect.SetRight( maTrackRect.Right() ); + } + else + mnThumbPixRange = 0; + } + + mbCalcSize = false; + + Size aNewSize = getCurrentCalcSize(); + if (aOldSize != aNewSize) + { + queue_resize(); + } + } + + if ( mnThumbPixRange ) + { + // Calculate values + if ( (mnVisibleSize >= (mnMaxRange-mnMinRange)) || + ((mnMaxRange-mnMinRange) <= 0) ) + { + mnThumbPos = mnMinRange; + mnThumbPixPos = 0; + mnThumbPixSize = mnThumbPixRange; + } + else + { + if ( mnVisibleSize ) + mnThumbPixSize = ImplMulDiv( mnThumbPixRange, mnVisibleSize, mnMaxRange-mnMinRange ); + else + { + if ( GetStyle() & WB_HORZ ) + mnThumbPixSize = maThumbRect.GetWidth(); + else + mnThumbPixSize = maThumbRect.GetHeight(); + } + if ( mnThumbPixSize < nMinThumbSize ) + mnThumbPixSize = nMinThumbSize; + if ( mnThumbPixSize > mnThumbPixRange ) + mnThumbPixSize = mnThumbPixRange; + mnThumbPixPos = ImplCalcThumbPosPix( mnThumbPos ); + } + } + + // If we're ought to output again and we have been triggered + // a Paint event via an Action, we don't output directly, + // but invalidate everything + if ( bUpdate && HasPaintEvent() ) + { + Invalidate(); + bUpdate = false; + } + ImplUpdateRects( bUpdate ); +} + +void ScrollBar::Draw( OutputDevice* pDev, const Point& rPos, SystemTextColorFlags nFlags ) +{ + Point aPos = pDev->LogicToPixel( rPos ); + + pDev->Push(); + pDev->SetMapMode(); + if ( !(nFlags & SystemTextColorFlags::Mono) ) + { + // DecoView uses the FaceColor... + AllSettings aSettings = pDev->GetSettings(); + StyleSettings aStyleSettings = aSettings.GetStyleSettings(); + if ( IsControlBackground() ) + aStyleSettings.SetFaceColor( GetControlBackground() ); + else + aStyleSettings.SetFaceColor( GetSettings().GetStyleSettings().GetFaceColor() ); + + aSettings.SetStyleSettings( aStyleSettings ); + pDev->SetSettings( aSettings ); + } + + // For printing: + // - calculate the size of the rects + // - because this is zero-based add the correct offset + // - print + // - force recalculate + + if ( mbCalcSize ) + ImplCalc( false ); + + maBtn1Rect+=aPos; + maBtn2Rect+=aPos; + maThumbRect+=aPos; + maTrackRect+=aPos; + maPage1Rect+=aPos; + maPage2Rect+=aPos; + + ImplDraw(*pDev); + pDev->Pop(); + + mbCalcSize = true; +} + +bool ScrollBar::ImplDrawNative(vcl::RenderContext& rRenderContext, sal_uInt16 nSystemTextColorFlags) +{ + ScrollbarValue scrValue; + + bool bNativeOK = rRenderContext.IsNativeControlSupported(ControlType::Scrollbar, ControlPart::Entire); + if (!bNativeOK) + return false; + + bool bHorz = (GetStyle() & WB_HORZ) != 0; + + // Draw the entire background if the control supports it + if (rRenderContext.IsNativeControlSupported(ControlType::Scrollbar, bHorz ? ControlPart::DrawBackgroundHorz : ControlPart::DrawBackgroundVert)) + { + ControlState nState = (IsEnabled() ? ControlState::ENABLED : ControlState::NONE) + | (HasFocus() ? ControlState::FOCUSED : ControlState::NONE); + + scrValue.mnMin = mnMinRange; + scrValue.mnMax = mnMaxRange; + scrValue.mnCur = mnThumbPos; + scrValue.mnVisibleSize = mnVisibleSize; + scrValue.maThumbRect = maThumbRect; + scrValue.maButton1Rect = maBtn1Rect; + scrValue.maButton2Rect = maBtn2Rect; + scrValue.mnButton1State = ((mnStateFlags & SCRBAR_STATE_BTN1_DOWN) ? ControlState::PRESSED : ControlState::NONE) | + ((!(mnStateFlags & SCRBAR_STATE_BTN1_DISABLE)) ? ControlState::ENABLED : ControlState::NONE); + scrValue.mnButton2State = ((mnStateFlags & SCRBAR_STATE_BTN2_DOWN) ? ControlState::PRESSED : ControlState::NONE) | + ((!(mnStateFlags & SCRBAR_STATE_BTN2_DISABLE)) ? ControlState::ENABLED : ControlState::NONE); + scrValue.mnThumbState = nState | ((mnStateFlags & SCRBAR_STATE_THUMB_DOWN) ? ControlState::PRESSED : ControlState::NONE); + + if (IsMouseOver()) + { + tools::Rectangle* pRect = ImplFindPartRect(GetPointerPosPixel()); + if (pRect) + { + if (pRect == &maThumbRect) + scrValue.mnThumbState |= ControlState::ROLLOVER; + else if (pRect == &maBtn1Rect) + scrValue.mnButton1State |= ControlState::ROLLOVER; + else if (pRect == &maBtn2Rect) + scrValue.mnButton2State |= ControlState::ROLLOVER; + } + } + + tools::Rectangle aCtrlRegion; + aCtrlRegion.Union(maBtn1Rect); + aCtrlRegion.Union(maBtn2Rect); + aCtrlRegion.Union(maPage1Rect); + aCtrlRegion.Union(maPage2Rect); + aCtrlRegion.Union(maThumbRect); + + tools::Rectangle aRequestedRegion(Point(0,0), GetOutputSizePixel()); + // if the actual native control region is smaller then the region that + // we requested the control to draw in, then draw a background rectangle + // to avoid drawing artifacts in the uncovered region + if (aCtrlRegion.GetWidth() < aRequestedRegion.GetWidth() || + aCtrlRegion.GetHeight() < aRequestedRegion.GetHeight()) + { + Color aFaceColor = rRenderContext.GetSettings().GetStyleSettings().GetFaceColor(); + rRenderContext.SetFillColor(aFaceColor); + rRenderContext.SetLineColor(aFaceColor); + rRenderContext.DrawRect(aRequestedRegion); + } + + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Scrollbar, (bHorz ? ControlPart::DrawBackgroundHorz : ControlPart::DrawBackgroundVert), + aCtrlRegion, nState, scrValue, OUString()); + } + else + { + if ((nSystemTextColorFlags & SCRBAR_DRAW_PAGE1) || (nSystemTextColorFlags & SCRBAR_DRAW_PAGE2)) + { + ControlPart part1 = bHorz ? ControlPart::TrackHorzLeft : ControlPart::TrackVertUpper; + ControlPart part2 = bHorz ? ControlPart::TrackHorzRight : ControlPart::TrackVertLower; + tools::Rectangle aCtrlRegion1(maPage1Rect); + tools::Rectangle aCtrlRegion2(maPage2Rect); + ControlState nState1 = (IsEnabled() ? ControlState::ENABLED : ControlState::NONE) + | (HasFocus() ? ControlState::FOCUSED : ControlState::NONE); + ControlState nState2 = nState1; + + nState1 |= ((mnStateFlags & SCRBAR_STATE_PAGE1_DOWN) ? ControlState::PRESSED : ControlState::NONE); + nState2 |= ((mnStateFlags & SCRBAR_STATE_PAGE2_DOWN) ? ControlState::PRESSED : ControlState::NONE); + + if (IsMouseOver()) + { + tools::Rectangle* pRect = ImplFindPartRect(GetPointerPosPixel()); + if (pRect) + { + if (pRect == &maPage1Rect) + nState1 |= ControlState::ROLLOVER; + else if (pRect == &maPage2Rect) + nState2 |= ControlState::ROLLOVER; + } + } + + if (nSystemTextColorFlags & SCRBAR_DRAW_PAGE1) + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Scrollbar, part1, aCtrlRegion1, nState1, scrValue, OUString()); + + if (nSystemTextColorFlags & SCRBAR_DRAW_PAGE2) + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Scrollbar, part2, aCtrlRegion2, nState2, scrValue, OUString()); + } + if ((nSystemTextColorFlags & SCRBAR_DRAW_BTN1) || (nSystemTextColorFlags & SCRBAR_DRAW_BTN2)) + { + ControlPart part1 = bHorz ? ControlPart::ButtonLeft : ControlPart::ButtonUp; + ControlPart part2 = bHorz ? ControlPart::ButtonRight : ControlPart::ButtonDown; + tools::Rectangle aCtrlRegion1(maBtn1Rect); + tools::Rectangle aCtrlRegion2(maBtn2Rect); + ControlState nState1 = HasFocus() ? ControlState::FOCUSED : ControlState::NONE; + ControlState nState2 = nState1; + + if (!Window::IsEnabled() || !IsEnabled()) + nState1 = (nState2 &= ~ControlState::ENABLED); + else + nState1 = (nState2 |= ControlState::ENABLED); + + nState1 |= ((mnStateFlags & SCRBAR_STATE_BTN1_DOWN) ? ControlState::PRESSED : ControlState::NONE); + nState2 |= ((mnStateFlags & SCRBAR_STATE_BTN2_DOWN) ? ControlState::PRESSED : ControlState::NONE); + + if (mnStateFlags & SCRBAR_STATE_BTN1_DISABLE) + nState1 &= ~ControlState::ENABLED; + if (mnStateFlags & SCRBAR_STATE_BTN2_DISABLE) + nState2 &= ~ControlState::ENABLED; + + if (IsMouseOver()) + { + tools::Rectangle* pRect = ImplFindPartRect(GetPointerPosPixel()); + if (pRect) + { + if (pRect == &maBtn1Rect) + nState1 |= ControlState::ROLLOVER; + else if (pRect == &maBtn2Rect) + nState2 |= ControlState::ROLLOVER; + } + } + + if (nSystemTextColorFlags & SCRBAR_DRAW_BTN1) + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Scrollbar, part1, aCtrlRegion1, nState1, scrValue, OUString()); + + if (nSystemTextColorFlags & SCRBAR_DRAW_BTN2) + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Scrollbar, part2, aCtrlRegion2, nState2, scrValue, OUString()); + } + if ((nSystemTextColorFlags & SCRBAR_DRAW_THUMB) && !maThumbRect.IsEmpty()) + { + ControlState nState = IsEnabled() ? ControlState::ENABLED : ControlState::NONE; + tools::Rectangle aCtrlRegion(maThumbRect); + + if (mnStateFlags & SCRBAR_STATE_THUMB_DOWN) + nState |= ControlState::PRESSED; + + if (HasFocus()) + nState |= ControlState::FOCUSED; + + if (IsMouseOver()) + { + tools::Rectangle* pRect = ImplFindPartRect(GetPointerPosPixel()); + if (pRect && pRect == &maThumbRect) + nState |= ControlState::ROLLOVER; + } + + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Scrollbar, (bHorz ? ControlPart::ThumbHorz : ControlPart::ThumbVert), + aCtrlRegion, nState, scrValue, OUString()); + } + } + return bNativeOK; +} + +void ScrollBar::ImplDraw(vcl::RenderContext& rRenderContext) +{ + DecorationView aDecoView(&rRenderContext); + tools::Rectangle aTempRect; + DrawButtonFlags nStyle; + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + SymbolType eSymbolType; + bool bEnabled = IsEnabled(); + + // Finish some open calculations (if any) + if (mbCalcSize) + ImplCalc(false); + + //vcl::Window *pWin = NULL; + //if (rRenderContext.GetOutDevType() == OUTDEV_WINDOW) + // pWin = static_cast<vcl::Window*>(&rRenderContext); + + // Draw the entire control if the native theme engine needs it + if (rRenderContext.IsNativeControlSupported(ControlType::Scrollbar, ControlPart::DrawBackgroundHorz)) + { + ImplDrawNative(rRenderContext, SCRBAR_DRAW_BACKGROUND); + return; + } + + if (!ImplDrawNative(rRenderContext, SCRBAR_DRAW_BTN1)) + { + nStyle = DrawButtonFlags::NoLightBorder; + if (mnStateFlags & SCRBAR_STATE_BTN1_DOWN) + nStyle |= DrawButtonFlags::Pressed; + aTempRect = aDecoView.DrawButton( PixelToLogic(maBtn1Rect), nStyle ); + ImplCalcSymbolRect( aTempRect ); + DrawSymbolFlags nSymbolStyle = DrawSymbolFlags::NONE; + if ((mnStateFlags & SCRBAR_STATE_BTN1_DISABLE) || !bEnabled) + nSymbolStyle |= DrawSymbolFlags::Disable; + if (GetStyle() & WB_HORZ) + eSymbolType = SymbolType::SPIN_LEFT; + else + eSymbolType = SymbolType::SPIN_UP; + aDecoView.DrawSymbol(aTempRect, eSymbolType, rStyleSettings.GetButtonTextColor(), nSymbolStyle); + } + + if (!ImplDrawNative(rRenderContext, SCRBAR_DRAW_BTN2)) + { + nStyle = DrawButtonFlags::NoLightBorder; + if (mnStateFlags & SCRBAR_STATE_BTN2_DOWN) + nStyle |= DrawButtonFlags::Pressed; + aTempRect = aDecoView.DrawButton(PixelToLogic(maBtn2Rect), nStyle); + ImplCalcSymbolRect(aTempRect); + DrawSymbolFlags nSymbolStyle = DrawSymbolFlags::NONE; + if ((mnStateFlags & SCRBAR_STATE_BTN2_DISABLE) || !bEnabled) + nSymbolStyle |= DrawSymbolFlags::Disable; + if (GetStyle() & WB_HORZ) + eSymbolType = SymbolType::SPIN_RIGHT; + else + eSymbolType = SymbolType::SPIN_DOWN; + aDecoView.DrawSymbol(aTempRect, eSymbolType, rStyleSettings.GetButtonTextColor(), nSymbolStyle); + } + + rRenderContext.SetLineColor(); + + if (!ImplDrawNative(rRenderContext, SCRBAR_DRAW_THUMB)) + { + if (!maThumbRect.IsEmpty()) + { + if (bEnabled) + { + nStyle = DrawButtonFlags::NoLightBorder; + aTempRect = aDecoView.DrawButton(PixelToLogic(maThumbRect), nStyle); + } + else + { + rRenderContext.SetFillColor(rStyleSettings.GetCheckedColor()); + rRenderContext.DrawRect(PixelToLogic(maThumbRect)); + } + } + } + + if (!ImplDrawNative(rRenderContext, SCRBAR_DRAW_PAGE1)) + { + if (mnStateFlags & SCRBAR_STATE_PAGE1_DOWN) + rRenderContext.SetFillColor(rStyleSettings.GetShadowColor()); + else + rRenderContext.SetFillColor(rStyleSettings.GetCheckedColor()); + rRenderContext.DrawRect(PixelToLogic(maPage1Rect)); + } + if (!ImplDrawNative(rRenderContext, SCRBAR_DRAW_PAGE2)) + { + if (mnStateFlags & SCRBAR_STATE_PAGE2_DOWN) + rRenderContext.SetFillColor(rStyleSettings.GetShadowColor()); + else + rRenderContext.SetFillColor(rStyleSettings.GetCheckedColor()); + rRenderContext.DrawRect(PixelToLogic(maPage2Rect)); + } +} + +tools::Long ScrollBar::ImplScroll( tools::Long nNewPos, bool bCallEndScroll ) +{ + tools::Long nOldPos = mnThumbPos; + SetThumbPos( nNewPos ); + tools::Long nDelta = mnThumbPos-nOldPos; + if ( nDelta ) + { + mnDelta = nDelta; + Scroll(); + if ( bCallEndScroll ) + EndScroll(); + mnDelta = 0; + } + return nDelta; +} + +tools::Long ScrollBar::ImplDoAction( bool bCallEndScroll ) +{ + tools::Long nDelta = 0; + + switch ( meScrollType ) + { + case ScrollType::LineUp: + nDelta = ImplScroll( mnThumbPos-mnLineSize, bCallEndScroll ); + break; + + case ScrollType::LineDown: + nDelta = ImplScroll( mnThumbPos+mnLineSize, bCallEndScroll ); + break; + + case ScrollType::PageUp: + nDelta = ImplScroll( mnThumbPos-mnPageSize, bCallEndScroll ); + break; + + case ScrollType::PageDown: + nDelta = ImplScroll( mnThumbPos+mnPageSize, bCallEndScroll ); + break; + default: + ; + } + + return nDelta; +} + +void ScrollBar::ImplDoMouseAction( const Point& rMousePos, bool bCallAction ) +{ + sal_uInt16 nOldStateFlags = mnStateFlags; + bool bAction = false; + bool bHorizontal = ( GetStyle() & WB_HORZ ) != 0; + bool bIsInside = false; + + Point aPoint( 0, 0 ); + tools::Rectangle aControlRegion( aPoint, GetOutputSizePixel() ); + + switch ( meScrollType ) + { + case ScrollType::LineUp: + if ( GetOutDev()->HitTestNativeScrollbar( bHorizontal? (IsRTLEnabled()? ControlPart::ButtonRight: ControlPart::ButtonLeft): ControlPart::ButtonUp, + aControlRegion, rMousePos, bIsInside )? + bIsInside: + maBtn1Rect.Contains( rMousePos ) ) + { + bAction = bCallAction; + mnStateFlags |= SCRBAR_STATE_BTN1_DOWN; + } + else + mnStateFlags &= ~SCRBAR_STATE_BTN1_DOWN; + break; + + case ScrollType::LineDown: + if ( GetOutDev()->HitTestNativeScrollbar( bHorizontal? (IsRTLEnabled()? ControlPart::ButtonLeft: ControlPart::ButtonRight): ControlPart::ButtonDown, + aControlRegion, rMousePos, bIsInside )? + bIsInside: + maBtn2Rect.Contains( rMousePos ) ) + { + bAction = bCallAction; + mnStateFlags |= SCRBAR_STATE_BTN2_DOWN; + } + else + mnStateFlags &= ~SCRBAR_STATE_BTN2_DOWN; + break; + + case ScrollType::PageUp: + // HitTestNativeScrollbar, see remark at top of file + if ( GetOutDev()->HitTestNativeScrollbar( bHorizontal? ControlPart::TrackHorzLeft: ControlPart::TrackVertUpper, + maPage1Rect, rMousePos, bIsInside )? + bIsInside: + maPage1Rect.Contains( rMousePos ) ) + { + bAction = bCallAction; + mnStateFlags |= SCRBAR_STATE_PAGE1_DOWN; + } + else + mnStateFlags &= ~SCRBAR_STATE_PAGE1_DOWN; + break; + + case ScrollType::PageDown: + // HitTestNativeScrollbar, see remark at top of file + if ( GetOutDev()->HitTestNativeScrollbar( bHorizontal? ControlPart::TrackHorzRight: ControlPart::TrackVertLower, + maPage2Rect, rMousePos, bIsInside )? + bIsInside: + maPage2Rect.Contains( rMousePos ) ) + { + bAction = bCallAction; + mnStateFlags |= SCRBAR_STATE_PAGE2_DOWN; + } + else + mnStateFlags &= ~SCRBAR_STATE_PAGE2_DOWN; + break; + default: + ; + } + + if ( nOldStateFlags != mnStateFlags ) + Invalidate(); + if ( bAction ) + ImplDoAction( false ); +} + +void ScrollBar::ImplDragThumb( const Point& rMousePos ) +{ + tools::Long nMovePix; + if ( GetStyle() & WB_HORZ ) + nMovePix = rMousePos.X()-(maThumbRect.Left()+mnMouseOff); + else + nMovePix = rMousePos.Y()-(maThumbRect.Top()+mnMouseOff); + + // Move thumb if necessary + if ( !nMovePix ) + return; + + mnThumbPixPos += nMovePix; + if ( mnThumbPixPos < 0 ) + mnThumbPixPos = 0; + if ( mnThumbPixPos > (mnThumbPixRange-mnThumbPixSize) ) + mnThumbPixPos = mnThumbPixRange-mnThumbPixSize; + tools::Long nOldPos = mnThumbPos; + mnThumbPos = ImplCalcThumbPos( mnThumbPixPos ); + ImplUpdateRects(); + if ( !(mbFullDrag && (nOldPos != mnThumbPos)) ) + return; + + // When dragging in windows the repaint request gets starved so dragging + // the scrollbar feels slower than it actually is. Let's force an immediate + // repaint of the scrollbar. + if (SupportsDoubleBuffering()) + { + Invalidate(); + PaintImmediately(); + } + else + ImplDraw(*GetOutDev()); + + mnDelta = mnThumbPos-nOldPos; + Scroll(); + mnDelta = 0; +} + +void ScrollBar::MouseButtonDown( const MouseEvent& rMEvt ) +{ + bool bPrimaryWarps = GetSettings().GetStyleSettings().GetPrimaryButtonWarpsSlider(); + bool bWarp = bPrimaryWarps ? rMEvt.IsLeft() : rMEvt.IsMiddle(); + bool bPrimaryWarping = bWarp && rMEvt.IsLeft(); + bool bPage = bPrimaryWarps ? rMEvt.IsRight() : rMEvt.IsLeft(); + + if (!rMEvt.IsLeft() && !rMEvt.IsMiddle() && !rMEvt.IsRight()) + return; + + Point aPosPixel; + if (!IsMapModeEnabled() && GetMapMode().GetMapUnit() == MapUnit::MapTwip) + { + // rMEvt coordinates are in twips. + GetOutDev()->Push(vcl::PushFlags::MAPMODE); + EnableMapMode(); + MapMode aMapMode = GetMapMode(); + aMapMode.SetOrigin(Point(0, 0)); + SetMapMode(aMapMode); + aPosPixel = LogicToPixel(rMEvt.GetPosPixel()); + GetOutDev()->Pop(); + } + const Point& rMousePos = (GetMapMode().GetMapUnit() != MapUnit::MapTwip ? rMEvt.GetPosPixel() : aPosPixel); + StartTrackingFlags nTrackFlags = StartTrackingFlags::NONE; + bool bHorizontal = ( GetStyle() & WB_HORZ ) != 0; + bool bIsInside = false; + bool bDragToMouse = false; + + Point aPoint( 0, 0 ); + tools::Rectangle aControlRegion( aPoint, GetOutputSizePixel() ); + + if ( GetOutDev()->HitTestNativeScrollbar( bHorizontal? (IsRTLEnabled()? ControlPart::ButtonRight: ControlPart::ButtonLeft): ControlPart::ButtonUp, + aControlRegion, rMousePos, bIsInside )? + bIsInside: + maBtn1Rect.Contains( rMousePos ) ) + { + if (rMEvt.IsLeft() && !(mnStateFlags & SCRBAR_STATE_BTN1_DISABLE) ) + { + nTrackFlags = StartTrackingFlags::ButtonRepeat; + meScrollType = ScrollType::LineUp; + } + } + else if ( GetOutDev()->HitTestNativeScrollbar( bHorizontal? (IsRTLEnabled()? ControlPart::ButtonLeft: ControlPart::ButtonRight): ControlPart::ButtonDown, + aControlRegion, rMousePos, bIsInside )? + bIsInside: + maBtn2Rect.Contains( rMousePos ) ) + { + if (rMEvt.IsLeft() && !(mnStateFlags & SCRBAR_STATE_BTN2_DISABLE) ) + { + nTrackFlags = StartTrackingFlags::ButtonRepeat; + meScrollType = ScrollType::LineDown; + } + } + else + { + bool bThumbHit = GetOutDev()->HitTestNativeScrollbar( bHorizontal? ControlPart::ThumbHorz : ControlPart::ThumbVert, + maThumbRect, rMousePos, bIsInside ) + ? bIsInside : maThumbRect.Contains( rMousePos ); + + bool bThumbAction = bWarp || bPage; + + bool bDragHandling = bWarp || (bThumbHit && bThumbAction); + if( bDragHandling ) + { + if( mpData ) + { + mpData->mbHide = true; // disable focus blinking + if (HasFocus()) + { + mnStateFlags |= SCRBAR_DRAW_THUMB; // paint without focus + Invalidate(); + } + } + + if ( mnVisibleSize < mnMaxRange-mnMinRange ) + { + nTrackFlags = StartTrackingFlags::NONE; + meScrollType = ScrollType::Drag; + + // calculate mouse offset + if (bWarp && (!bThumbHit || !bPrimaryWarping)) + { + bDragToMouse = true; + if ( GetStyle() & WB_HORZ ) + mnMouseOff = maThumbRect.GetWidth()/2; + else + mnMouseOff = maThumbRect.GetHeight()/2; + } + else + { + if ( GetStyle() & WB_HORZ ) + mnMouseOff = rMousePos.X()-maThumbRect.Left(); + else + mnMouseOff = rMousePos.Y()-maThumbRect.Top(); + } + + mnStateFlags |= SCRBAR_STATE_THUMB_DOWN; + Invalidate(); + } + } + else if(bPage && (!GetOutDev()->HitTestNativeScrollbar( bHorizontal? ControlPart::TrackHorzArea : ControlPart::TrackVertArea, + aControlRegion, rMousePos, bIsInside ) || + bIsInside) ) + { + nTrackFlags = StartTrackingFlags::ButtonRepeat; + + // HitTestNativeScrollbar, see remark at top of file + if ( GetOutDev()->HitTestNativeScrollbar( bHorizontal? ControlPart::TrackHorzLeft : ControlPart::TrackVertUpper, + maPage1Rect, rMousePos, bIsInside )? + bIsInside: + maPage1Rect.Contains( rMousePos ) ) + { + meScrollType = ScrollType::PageUp; + } + else + { + meScrollType = ScrollType::PageDown; + } + } + } + + // Should we start Tracking? + if ( meScrollType == ScrollType::DontKnow ) + return; + + // store original position for cancel and EndScroll delta + mnStartPos = mnThumbPos; + // #92906# Call StartTracking() before ImplDoMouseAction(), otherwise + // MouseButtonUp() / EndTracking() may be called if somebody is spending + // a lot of time in the scroll handler + StartTracking( nTrackFlags ); + ImplDoMouseAction( rMousePos ); + + if( bDragToMouse ) + ImplDragThumb( rMousePos ); + +} + +void ScrollBar::Tracking( const TrackingEvent& rTEvt ) +{ + if ( rTEvt.IsTrackingEnded() ) + { + // Restore Button and PageRect status + sal_uInt16 nOldStateFlags = mnStateFlags; + mnStateFlags &= ~(SCRBAR_STATE_BTN1_DOWN | SCRBAR_STATE_BTN2_DOWN | + SCRBAR_STATE_PAGE1_DOWN | SCRBAR_STATE_PAGE2_DOWN | + SCRBAR_STATE_THUMB_DOWN); + if ( nOldStateFlags != mnStateFlags ) + Invalidate(); + + // Restore the old ThumbPosition when canceled + if ( rTEvt.IsTrackingCanceled() ) + { + tools::Long nOldPos = mnThumbPos; + SetThumbPos( mnStartPos ); + mnDelta = mnThumbPos-nOldPos; + Scroll(); + } + + if ( meScrollType == ScrollType::Drag ) + { + // On a SCROLLDRAG we recalculate the Thumb, so that it's back to a + // rounded ThumbPosition + ImplCalc(); + + if ( !mbFullDrag && (mnStartPos != mnThumbPos) ) + { + mnDelta = mnThumbPos-mnStartPos; + Scroll(); + mnDelta = 0; + } + } + + mnDelta = mnThumbPos-mnStartPos; + EndScroll(); + mnDelta = 0; + meScrollType = ScrollType::DontKnow; + + if( mpData ) + mpData->mbHide = false; // re-enable focus blinking + } + else + { + Point aPosPixel; + if (!IsMapModeEnabled() && GetMapMode().GetMapUnit() == MapUnit::MapTwip) + { + // rTEvt coordinates are in twips. + GetOutDev()->Push(vcl::PushFlags::MAPMODE); + EnableMapMode(); + MapMode aMapMode = GetMapMode(); + aMapMode.SetOrigin(Point(0, 0)); + SetMapMode(aMapMode); + aPosPixel = LogicToPixel(rTEvt.GetMouseEvent().GetPosPixel()); + GetOutDev()->Pop(); + } + const Point rMousePos = (GetMapMode().GetMapUnit() != MapUnit::MapTwip ? rTEvt.GetMouseEvent().GetPosPixel() : aPosPixel); + + // Dragging is treated in a special way + if ( meScrollType == ScrollType::Drag ) + ImplDragThumb( rMousePos ); + else + ImplDoMouseAction( rMousePos, rTEvt.IsTrackingRepeat() ); + + // If ScrollBar values are translated in a way that there's + // nothing left to track, we cancel here + if ( !IsVisible() || (mnVisibleSize >= (mnMaxRange-mnMinRange)) ) + EndTracking(); + } +} + +void ScrollBar::KeyInput( const KeyEvent& rKEvt ) +{ + if ( !rKEvt.GetKeyCode().GetModifier() ) + { + switch ( rKEvt.GetKeyCode().GetCode() ) + { + case KEY_HOME: + DoScroll( 0 ); + break; + + case KEY_END: + DoScroll( GetRangeMax() ); + break; + + case KEY_LEFT: + case KEY_UP: + DoScrollAction( ScrollType::LineUp ); + break; + + case KEY_RIGHT: + case KEY_DOWN: + DoScrollAction( ScrollType::LineDown ); + break; + + case KEY_PAGEUP: + DoScrollAction( ScrollType::PageUp ); + break; + + case KEY_PAGEDOWN: + DoScrollAction( ScrollType::PageDown ); + break; + + default: + Control::KeyInput( rKEvt ); + break; + } + } + else + Control::KeyInput( rKEvt ); +} + +void ScrollBar::ApplySettings(vcl::RenderContext& rRenderContext) +{ + rRenderContext.SetBackground(); +} + +void ScrollBar::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&) +{ + ImplDraw(rRenderContext); +} + +void ScrollBar::Move() +{ + Control::Move(); + mbCalcSize = true; + if (IsReallyVisible()) + ImplCalc(false); + Invalidate(); +} + +void ScrollBar::Resize() +{ + Control::Resize(); + mbCalcSize = true; + if ( IsReallyVisible() ) + ImplCalc( false ); + Invalidate(); +} + +IMPL_LINK_NOARG(ScrollBar, ImplAutoTimerHdl, Timer *, void) +{ + if( mpData && mpData->mbHide ) + return; + ImplInvert(); +} + +void ScrollBar::ImplInvert() +{ + tools::Rectangle aRect( maThumbRect ); + if( aRect.getWidth() > 4 ) + { + aRect.AdjustLeft(2 ); + aRect.AdjustRight( -2 ); + } + if( aRect.getHeight() > 4 ) + { + aRect.AdjustTop(2 ); + aRect.AdjustBottom( -2 ); + } + + GetOutDev()->Invert( aRect ); +} + +void ScrollBar::GetFocus() +{ + if( !mpData ) + { + mpData.reset(new ImplScrollBarData); + mpData->maTimer.SetInvokeHandler( LINK( this, ScrollBar, ImplAutoTimerHdl ) ); + mpData->mbHide = false; + } + ImplInvert(); // react immediately + mpData->maTimer.SetTimeout( GetSettings().GetStyleSettings().GetCursorBlinkTime() ); + mpData->maTimer.Start(); + Control::GetFocus(); +} + +void ScrollBar::LoseFocus() +{ + if( mpData ) + mpData->maTimer.Stop(); + Invalidate(); + + Control::LoseFocus(); +} + +void ScrollBar::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( nType == StateChangedType::InitShow ) + ImplCalc( false ); + else if ( nType == StateChangedType::Data ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + ImplCalc(); + } + else if ( nType == StateChangedType::UpdateMode ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + { + ImplCalc( false ); + Invalidate(); + } + } + else if ( nType == StateChangedType::Enable ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + ImplInitStyle( GetStyle() ); + if ( IsReallyVisible() && IsUpdateMode() ) + { + if ( (GetPrevStyle() & SCRBAR_VIEW_STYLE) != + (GetStyle() & SCRBAR_VIEW_STYLE) ) + { + mbCalcSize = true; + ImplCalc( false ); + Invalidate(); + } + } + } +} + +void ScrollBar::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) ) + { + mbCalcSize = true; + ImplCalc( false ); + Invalidate(); + } +} + +tools::Rectangle* ScrollBar::ImplFindPartRect( const Point& rPt ) +{ + bool bHorizontal = ( GetStyle() & WB_HORZ ) != 0; + bool bIsInside = false; + + Point aPoint( 0, 0 ); + tools::Rectangle aControlRegion( aPoint, GetOutputSizePixel() ); + + if( GetOutDev()->HitTestNativeScrollbar( bHorizontal? (IsRTLEnabled()? ControlPart::ButtonRight: ControlPart::ButtonLeft): ControlPart::ButtonUp, + aControlRegion, rPt, bIsInside )? + bIsInside: + maBtn1Rect.Contains( rPt ) ) + return &maBtn1Rect; + else if( GetOutDev()->HitTestNativeScrollbar( bHorizontal? (IsRTLEnabled()? ControlPart::ButtonLeft: ControlPart::ButtonRight): ControlPart::ButtonDown, + aControlRegion, rPt, bIsInside )? + bIsInside: + maBtn2Rect.Contains( rPt ) ) + return &maBtn2Rect; + // HitTestNativeScrollbar, see remark at top of file + else if( GetOutDev()->HitTestNativeScrollbar( bHorizontal ? ControlPart::TrackHorzLeft : ControlPart::TrackVertUpper, + maPage1Rect, rPt, bIsInside)? + bIsInside: + maPage1Rect.Contains( rPt ) ) + return &maPage1Rect; + // HitTestNativeScrollbar, see remark at top of file + else if( GetOutDev()->HitTestNativeScrollbar( bHorizontal ? ControlPart::TrackHorzRight : ControlPart::TrackVertLower, + maPage2Rect, rPt, bIsInside)? + bIsInside: + maPage2Rect.Contains( rPt ) ) + return &maPage2Rect; + // HitTestNativeScrollbar, see remark at top of file + else if( GetOutDev()->HitTestNativeScrollbar( bHorizontal ? ControlPart::ThumbHorz : ControlPart::ThumbVert, + maThumbRect, rPt, bIsInside)? + bIsInside: + maThumbRect.Contains( rPt ) ) + return &maThumbRect; + else + return nullptr; +} + +bool ScrollBar::PreNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE ) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if( pMouseEvt && !pMouseEvt->GetButtons() && !pMouseEvt->IsSynthetic() && !pMouseEvt->IsModifierChanged() ) + { + // Trigger a redraw if mouse over state has changed + if( IsNativeControlSupported(ControlType::Scrollbar, ControlPart::Entire) ) + { + tools::Rectangle* pRect = ImplFindPartRect( GetPointerPosPixel() ); + tools::Rectangle* pLastRect = ImplFindPartRect( GetLastPointerPosPixel() ); + if( pRect != pLastRect || pMouseEvt->IsLeaveWindow() || pMouseEvt->IsEnterWindow() ) + { + vcl::Region aRgn( GetOutDev()->GetActiveClipRegion() ); + vcl::Region aClipRegion; + + if ( pRect ) + aClipRegion.Union( *pRect ); + if ( pLastRect ) + aClipRegion.Union( *pLastRect ); + + // Support for 3-button scroll bars + bool bHas3Buttons = IsNativeControlSupported( ControlType::Scrollbar, ControlPart::HasThreeButtons ); + if ( bHas3Buttons && ( pRect == &maBtn1Rect || pLastRect == &maBtn1Rect ) ) + { + aClipRegion.Union( maBtn2Rect ); + } + + GetOutDev()->SetClipRegion( aClipRegion ); + Invalidate(aClipRegion.GetBoundRect()); + + GetOutDev()->SetClipRegion( aRgn ); + } + } + } + } + + return Control::PreNotify(rNEvt); +} + +void ScrollBar::Scroll() +{ + ImplCallEventListenersAndHandler( VclEventId::ScrollbarScroll, [this] () { maScrollHdl.Call(this); } ); +} + +void ScrollBar::EndScroll() +{ + ImplCallEventListenersAndHandler( VclEventId::ScrollbarEndScroll, [this] () { maEndScrollHdl.Call(this); } ); +} + +tools::Long ScrollBar::DoScroll( tools::Long nNewPos ) +{ + if ( meScrollType != ScrollType::DontKnow ) + return 0; + + SAL_INFO("vcl.scrollbar", "DoScroll(" << nNewPos << ")"); + meScrollType = ScrollType::Drag; + tools::Long nDelta = ImplScroll( nNewPos, true ); + meScrollType = ScrollType::DontKnow; + return nDelta; +} + +tools::Long ScrollBar::DoScrollAction( ScrollType eScrollType ) +{ + if ( (meScrollType != ScrollType::DontKnow) || + (eScrollType == ScrollType::DontKnow) || + (eScrollType == ScrollType::Drag) ) + return 0; + + meScrollType = eScrollType; + tools::Long nDelta = ImplDoAction( true ); + meScrollType = ScrollType::DontKnow; + return nDelta; +} + +void ScrollBar::SetRangeMin( tools::Long nNewRange ) +{ + SetRange( Range( nNewRange, GetRangeMax() ) ); +} + +void ScrollBar::SetRangeMax( tools::Long nNewRange ) +{ + SetRange( Range( GetRangeMin(), nNewRange ) ); +} + +void ScrollBar::SetRange( const Range& rRange ) +{ + // Adapt Range + Range aRange = rRange; + aRange.Justify(); + tools::Long nNewMinRange = aRange.Min(); + tools::Long nNewMaxRange = aRange.Max(); + + // If Range differs, set a new one + if ( (mnMinRange == nNewMinRange) && (mnMaxRange == nNewMaxRange)) + return; + + mnMinRange = nNewMinRange; + mnMaxRange = nNewMaxRange; + + // Adapt Thumb + if ( mnThumbPos > mnMaxRange-mnVisibleSize ) + mnThumbPos = mnMaxRange-mnVisibleSize; + if ( mnThumbPos < mnMinRange ) + mnThumbPos = mnMinRange; + + CompatStateChanged( StateChangedType::Data ); +} + +void ScrollBar::SetThumbPos( tools::Long nNewThumbPos ) +{ + if ( nNewThumbPos > mnMaxRange-mnVisibleSize ) + nNewThumbPos = mnMaxRange-mnVisibleSize; + if ( nNewThumbPos < mnMinRange ) + nNewThumbPos = mnMinRange; + + if ( mnThumbPos != nNewThumbPos ) + { + mnThumbPos = nNewThumbPos; + CompatStateChanged( StateChangedType::Data ); + } +} + +void ScrollBar::SetVisibleSize( tools::Long nNewSize ) +{ + if ( mnVisibleSize != nNewSize ) + { + mnVisibleSize = nNewSize; + + // Adapt Thumb + if ( mnThumbPos > mnMaxRange-mnVisibleSize ) + mnThumbPos = mnMaxRange-mnVisibleSize; + if ( mnThumbPos < mnMinRange ) + mnThumbPos = mnMinRange; + CompatStateChanged( StateChangedType::Data ); + } +} + +Size ScrollBar::GetOptimalSize() const +{ + if (mbCalcSize) + const_cast<ScrollBar*>(this)->ImplCalc(false); + + Size aRet = getCurrentCalcSize(); + + const tools::Long nMinThumbSize = GetSettings().GetStyleSettings().GetMinThumbSize(); + + if (GetStyle() & WB_HORZ) + { + aRet.setWidth( maBtn1Rect.GetWidth() + nMinThumbSize + maBtn2Rect.GetWidth() ); + } + else + { + aRet.setHeight( maBtn1Rect.GetHeight() + nMinThumbSize + maBtn2Rect.GetHeight() ); + } + + return aRet; +} + +Size ScrollBar::getCurrentCalcSize() const +{ + tools::Rectangle aCtrlRegion; + aCtrlRegion.Union(maBtn1Rect); + aCtrlRegion.Union(maBtn2Rect); + aCtrlRegion.Union(maPage1Rect); + aCtrlRegion.Union(maPage2Rect); + aCtrlRegion.Union(maThumbRect); + return aCtrlRegion.GetSize(); +} + +void ScrollBarBox::ImplInit(vcl::Window* pParent, WinBits nStyle) +{ + Window::ImplInit( pParent, nStyle, nullptr ); + + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + tools::Long nScrollSize = rStyleSettings.GetScrollBarSize(); + SetSizePixel(Size(nScrollSize, nScrollSize)); +} + +ScrollBarBox::ScrollBarBox( vcl::Window* pParent, WinBits nStyle ) : + Window( WindowType::SCROLLBARBOX ) +{ + ImplInit( pParent, nStyle ); +} + +void ScrollBarBox::ApplySettings(vcl::RenderContext& rRenderContext) +{ + if (rRenderContext.IsBackground()) + { + Color aColor = rRenderContext.GetSettings().GetStyleSettings().GetFaceColor(); + ApplyControlBackground(rRenderContext, aColor); + } +} + +void ScrollBarBox::StateChanged( StateChangedType nType ) +{ + Window::StateChanged( nType ); + + if (nType == StateChangedType::ControlBackground) + { + Invalidate(); + } +} + +void ScrollBarBox::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Window::DataChanged( rDCEvt ); + + if ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) + { + Invalidate(); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/slider.cxx b/vcl/source/control/slider.cxx new file mode 100644 index 000000000..819b833e7 --- /dev/null +++ b/vcl/source/control/slider.cxx @@ -0,0 +1,905 @@ +/* -*- 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 <vcl/event.hxx> +#include <vcl/decoview.hxx> +#include <slider.hxx> +#include <vcl/settings.hxx> + +#include "thumbpos.hxx" + +#define SLIDER_STATE_CHANNEL1_DOWN (sal_uInt16(0x0001)) +#define SLIDER_STATE_CHANNEL2_DOWN (sal_uInt16(0x0002)) +#define SLIDER_STATE_THUMB_DOWN (sal_uInt16(0x0004)) + +#define SLIDER_THUMB_SIZE 9 +#define SLIDER_CHANNEL_SIZE 4 +#define SLIDER_CHANNEL_HALFSIZE 2 + +#define SLIDER_HEIGHT 16 + +#define SLIDER_VIEW_STYLE (WB_3DLOOK | WB_HORZ | WB_VERT) + +void Slider::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + mnThumbPixOffset = 0; + mnThumbPixRange = 0; + mnThumbPixPos = 0; // between mnThumbPixOffset and mnThumbPixOffset+mnThumbPixRange + mnThumbSize = SLIDER_THUMB_SIZE; + mnChannelPixRange = 0; + mnChannelPixTop = 0; + mnChannelPixBottom = 0; + + mnMinRange = 0; + mnMaxRange = 100; + mnThumbPos = 0; + mnLineSize = 1; + mnPageSize = 1; + mnStateFlags = 0; + meScrollType = ScrollType::DontKnow; + mbCalcSize = true; + + Control::ImplInit( pParent, nStyle, nullptr ); + + ImplInitSettings(); + SetSizePixel( CalcWindowSizePixel() ); +} + +Slider::Slider( vcl::Window* pParent, WinBits nStyle ) : + Control(WindowType::SLIDER) +{ + ImplInit( pParent, nStyle ); +} + +Slider::~Slider() +{ + disposeOnce(); +} + +void Slider::ImplInitSettings() +{ + vcl::Window* pParent = GetParent(); + if ( pParent->IsChildTransparentModeEnabled() && !IsControlBackground() ) + { + EnableChildTransparentMode(); + SetParentClipMode( ParentClipMode::NoClip ); + SetPaintTransparent( true ); + SetBackground(); + } + else + { + EnableChildTransparentMode( false ); + SetParentClipMode(); + SetPaintTransparent( false ); + + if ( IsControlBackground() ) + SetBackground( GetControlBackground() ); + else + SetBackground( pParent->GetBackground() ); + } +} + +void Slider::ImplUpdateRects( bool bUpdate ) +{ + tools::Rectangle aOldThumbRect = maThumbRect; + bool bInvalidateAll = false; + + if ( mnThumbPixRange ) + { + if ( GetStyle() & WB_HORZ ) + { + maThumbRect.SetLeft(mnThumbPixPos - (mnThumbSize / 2)); + maThumbRect.SetRight(maThumbRect.Left() + mnThumbSize - 1); + if ( 0 < maThumbRect.Left() ) + { + maChannel1Rect.SetLeft( 0 ); + maChannel1Rect.SetRight( maThumbRect.Left()-1 ); + maChannel1Rect.SetTop( mnChannelPixTop ); + maChannel1Rect.SetBottom( mnChannelPixBottom ); + } + else + maChannel1Rect.SetEmpty(); + if ( mnChannelPixRange-1 > maThumbRect.Right() ) + { + maChannel2Rect.SetLeft( maThumbRect.Right()+1 ); + maChannel2Rect.SetRight( mnChannelPixRange-1 ); + maChannel2Rect.SetTop( mnChannelPixTop ); + maChannel2Rect.SetBottom( mnChannelPixBottom ); + } + else + maChannel2Rect.SetEmpty(); + + const tools::Rectangle aControlRegion(tools::Rectangle(Point(), Size(mnThumbSize, 10))); + tools::Rectangle aThumbBounds, aThumbContent; + if ( GetNativeControlRegion( ControlType::Slider, ControlPart::ThumbHorz, + aControlRegion, ControlState::NONE, ImplControlValue(), + aThumbBounds, aThumbContent ) ) + { + maThumbRect.SetLeft( mnThumbPixPos - aThumbBounds.GetWidth()/2 ); + maThumbRect.SetRight( maThumbRect.Left() + aThumbBounds.GetWidth() - 1 ); + bInvalidateAll = true; + } + } + else + { + maThumbRect.SetTop( mnThumbPixPos - (mnThumbSize / 2)); + maThumbRect.SetBottom( maThumbRect.Top() + mnThumbSize - 1); + if ( 0 < maThumbRect.Top() ) + { + maChannel1Rect.SetTop( 0 ); + maChannel1Rect.SetBottom( maThumbRect.Top()-1 ); + maChannel1Rect.SetLeft( mnChannelPixTop ); + maChannel1Rect.SetRight( mnChannelPixBottom ); + } + else + maChannel1Rect.SetEmpty(); + if ( mnChannelPixRange-1 > maThumbRect.Bottom() ) + { + maChannel2Rect.SetTop( maThumbRect.Bottom()+1 ); + maChannel2Rect.SetBottom( mnChannelPixRange-1 ); + maChannel2Rect.SetLeft( mnChannelPixTop ); + maChannel2Rect.SetRight( mnChannelPixBottom ); + } + else + maChannel2Rect.SetEmpty(); + + const tools::Rectangle aControlRegion(tools::Rectangle(Point(), Size(10, mnThumbSize))); + tools::Rectangle aThumbBounds, aThumbContent; + if ( GetNativeControlRegion( ControlType::Slider, ControlPart::ThumbVert, + aControlRegion, ControlState::NONE, ImplControlValue(), + aThumbBounds, aThumbContent ) ) + { + maThumbRect.SetTop( mnThumbPixPos - aThumbBounds.GetHeight()/2 ); + maThumbRect.SetBottom( maThumbRect.Top() + aThumbBounds.GetHeight() - 1 ); + bInvalidateAll = true; + } + } + } + else + { + maChannel1Rect.SetEmpty(); + maChannel2Rect.SetEmpty(); + maThumbRect.SetEmpty(); + } + + if ( !bUpdate ) + return; + + if ( aOldThumbRect == maThumbRect ) + return; + + if( bInvalidateAll ) + Invalidate(InvalidateFlags::NoChildren | InvalidateFlags::NoErase); + else + { + vcl::Region aInvalidRegion( aOldThumbRect ); + aInvalidRegion.Union( maThumbRect ); + + if( !IsBackground() && GetParent() ) + { + const Point aPos( GetPosPixel() ); + aInvalidRegion.Move( aPos.X(), aPos.Y() ); + GetParent()->Invalidate( aInvalidRegion, InvalidateFlags::Transparent | InvalidateFlags::Update ); + } + else + Invalidate( aInvalidRegion ); + } +} + +tools::Long Slider::ImplCalcThumbPos( tools::Long nPixPos ) const +{ + // calculate position + tools::Long nCalcThumbPos; + nCalcThumbPos = ImplMulDiv( nPixPos-mnThumbPixOffset, mnMaxRange-mnMinRange, mnThumbPixRange-1 ); + nCalcThumbPos += mnMinRange; + return nCalcThumbPos; +} + +tools::Long Slider::ImplCalcThumbPosPix( tools::Long nPos ) const +{ + // calculate position + tools::Long nCalcThumbPos; + nCalcThumbPos = ImplMulDiv( nPos-mnMinRange, mnThumbPixRange-1, mnMaxRange-mnMinRange ); + // at the beginning and end we try to display Slider correctly + if ( !nCalcThumbPos && (mnThumbPos > mnMinRange) ) + nCalcThumbPos = 1; + if ( nCalcThumbPos && + (nCalcThumbPos == mnThumbPixRange-1) && + (mnThumbPos < mnMaxRange) ) + nCalcThumbPos--; + return nCalcThumbPos+mnThumbPixOffset; +} + +void Slider::ImplCalc( bool bUpdate ) +{ + bool bInvalidateAll = false; + + if (mbCalcSize) + { + if (GetStyle() & WB_HORZ) + { + const tools::Rectangle aControlRegion(tools::Rectangle(Point(), Size(SLIDER_THUMB_SIZE, 10))); + tools::Rectangle aThumbBounds, aThumbContent; + if (GetNativeControlRegion(ControlType::Slider, ControlPart::ThumbHorz, + aControlRegion, ControlState::NONE, ImplControlValue(), + aThumbBounds, aThumbContent)) + { + mnThumbSize = aThumbBounds.GetWidth(); + } + else + { + mnThumbSize = SLIDER_THUMB_SIZE; + } + } + else + { + const tools::Rectangle aControlRegion(tools::Rectangle(Point(), Size(10, SLIDER_THUMB_SIZE))); + tools::Rectangle aThumbBounds, aThumbContent; + if (GetNativeControlRegion( ControlType::Slider, ControlPart::ThumbVert, + aControlRegion, ControlState::NONE, ImplControlValue(), + aThumbBounds, aThumbContent)) + { + mnThumbSize = aThumbBounds.GetHeight(); + } + else + { + mnThumbSize = SLIDER_THUMB_SIZE; + } + } + + tools::Long nOldChannelPixRange = mnChannelPixRange; + tools::Long nOldChannelPixTop = mnChannelPixTop; + tools::Long nOldChannelPixBottom = mnChannelPixBottom; + tools::Long nCalcWidth; + tools::Long nCalcHeight; + + maChannel1Rect.SetEmpty(); + maChannel2Rect.SetEmpty(); + maThumbRect.SetEmpty(); + + Size aSize = GetOutputSizePixel(); + if ( GetStyle() & WB_HORZ ) + { + nCalcWidth = aSize.Width(); + nCalcHeight = aSize.Height(); + maThumbRect.SetTop( 0 ); + maThumbRect.SetBottom( aSize.Height()-1 ); + } + else + { + nCalcWidth = aSize.Height(); + nCalcHeight = aSize.Width(); + maThumbRect.SetLeft( 0 ); + maThumbRect.SetRight( aSize.Width()-1 ); + } + + if (nCalcWidth >= mnThumbSize) + { + mnThumbPixOffset = mnThumbSize / 2; + mnThumbPixRange = nCalcWidth - mnThumbSize; + mnThumbPixPos = 0; + mnChannelPixRange = nCalcWidth; + mnChannelPixTop = (nCalcHeight/2)-SLIDER_CHANNEL_HALFSIZE; + mnChannelPixBottom = mnChannelPixTop+SLIDER_CHANNEL_SIZE-1; + } + else + { + mnThumbPixRange = 0; + mnChannelPixRange = 0; + } + + if ( (nOldChannelPixRange != mnChannelPixRange) || + (nOldChannelPixTop != mnChannelPixTop) || + (nOldChannelPixBottom != mnChannelPixBottom) ) + bInvalidateAll = true; + + mbCalcSize = false; + } + + if ( mnThumbPixRange ) + mnThumbPixPos = ImplCalcThumbPosPix( mnThumbPos ); + + if ( bUpdate && bInvalidateAll ) + { + Invalidate(); + bUpdate = false; + } + ImplUpdateRects( bUpdate ); +} + +void Slider::ImplDraw(vcl::RenderContext& rRenderContext) +{ + // do missing calculations + if (mbCalcSize) + ImplCalc(false); + + ControlPart nPart = (GetStyle() & WB_HORZ) ? ControlPart::TrackHorzArea : ControlPart::TrackVertArea; + + if (rRenderContext.IsNativeControlSupported(ControlType::Slider, nPart)) + { + ControlState nState = (IsEnabled() ? ControlState::ENABLED : ControlState::NONE); + nState |= (HasFocus() ? ControlState::FOCUSED : ControlState::NONE); + + SliderValue aSliderValue; + aSliderValue.mnMin = mnMinRange; + aSliderValue.mnMax = mnMaxRange; + aSliderValue.mnCur = mnThumbPos; + aSliderValue.maThumbRect = maThumbRect; + + if (IsMouseOver()) + { + if (maThumbRect.Contains(GetPointerPosPixel())) + aSliderValue.mnThumbState |= ControlState::ROLLOVER; + } + + const tools::Rectangle aCtrlRegion(Point(0,0), GetOutputSizePixel()); + + if (rRenderContext.DrawNativeControl(ControlType::Slider, nPart, aCtrlRegion, nState, aSliderValue, OUString())) + return; + } + + DecorationView aDecoView(&rRenderContext); + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + bool bEnabled = IsEnabled(); + + if (!maChannel1Rect.IsEmpty()) + { + tools::Long nRectSize; + tools::Rectangle aRect = maChannel1Rect; + rRenderContext.SetLineColor(rStyleSettings.GetShadowColor()); + if (GetStyle() & WB_HORZ) + { + rRenderContext.DrawLine(aRect.TopLeft(), Point(aRect.Left(), aRect.Bottom() - 1)); + rRenderContext.DrawLine(aRect.TopLeft(), aRect.TopRight()); + } + else + { + rRenderContext.DrawLine(aRect.TopLeft(), Point(aRect.Right() - 1, aRect.Top())); + rRenderContext.DrawLine(aRect.TopLeft(), aRect.BottomLeft()); + } + rRenderContext.SetLineColor(rStyleSettings.GetLightColor()); + if (GetStyle() & WB_HORZ) + { + rRenderContext.DrawLine(aRect.BottomLeft(), aRect.BottomRight()); + nRectSize = aRect.GetWidth(); + } + else + { + rRenderContext.DrawLine(aRect.TopRight(), aRect.BottomRight()); + nRectSize = aRect.GetHeight(); + } + + if (nRectSize > 1) + { + aRect.AdjustLeft( 1 ); + aRect.AdjustTop( 1 ); + if (GetStyle() & WB_HORZ) + aRect.AdjustBottom( -1 ); + else + aRect.AdjustRight( -1 ); + rRenderContext.SetLineColor(); + if (mnStateFlags & SLIDER_STATE_CHANNEL1_DOWN) + rRenderContext.SetFillColor(rStyleSettings.GetShadowColor()); + else + rRenderContext.SetFillColor(rStyleSettings.GetCheckedColor()); + rRenderContext.DrawRect(aRect); + } + } + + if (!maChannel2Rect.IsEmpty()) + { + tools::Long nRectSize; + tools::Rectangle aRect = maChannel2Rect; + rRenderContext.SetLineColor(rStyleSettings.GetLightColor()); + if (GetStyle() & WB_HORZ) + { + rRenderContext.DrawLine(aRect.TopRight(), aRect.BottomRight()); + rRenderContext.DrawLine(aRect.BottomLeft(), aRect.BottomRight()); + nRectSize = aRect.GetWidth(); + } + else + { + rRenderContext.DrawLine(aRect.BottomLeft(), aRect.BottomRight()); + rRenderContext.DrawLine(aRect.TopRight(), aRect.BottomRight()); + nRectSize = aRect.GetHeight(); + } + + if (nRectSize > 1) + { + rRenderContext.SetLineColor(rStyleSettings.GetShadowColor()); + if (GetStyle() & WB_HORZ) + rRenderContext.DrawLine(aRect.TopLeft(), Point(aRect.Right() - 1, aRect.Top())); + else + rRenderContext.DrawLine(aRect.TopLeft(), Point(aRect.Left(), aRect.Bottom() - 1)); + + aRect.AdjustRight( -1 ); + aRect.AdjustBottom( -1 ); + if (GetStyle() & WB_HORZ) + aRect.AdjustTop( 1 ); + else + aRect.AdjustLeft( 1 ); + rRenderContext.SetLineColor(); + if (mnStateFlags & SLIDER_STATE_CHANNEL2_DOWN) + rRenderContext.SetFillColor(rStyleSettings.GetShadowColor()); + else + rRenderContext.SetFillColor(rStyleSettings.GetCheckedColor()); + rRenderContext.DrawRect(aRect); + } + } + + if (maThumbRect.IsEmpty()) + return; + + if (bEnabled) + { + DrawButtonFlags nStyle = DrawButtonFlags::NONE; + if (mnStateFlags & SLIDER_STATE_THUMB_DOWN) + nStyle |= DrawButtonFlags::Pressed; + aDecoView.DrawButton(maThumbRect, nStyle); + } + else + { + rRenderContext.SetLineColor(rStyleSettings.GetShadowColor()); + rRenderContext.SetFillColor(rStyleSettings.GetCheckedColor()); + rRenderContext.DrawRect(maThumbRect); + } +} + +bool Slider::ImplIsPageUp( const Point& rPos ) const +{ + Size aSize = GetOutputSizePixel(); + tools::Rectangle aRect = maChannel1Rect; + if ( GetStyle() & WB_HORZ ) + { + aRect.SetTop( 0 ); + aRect.SetBottom( aSize.Height()-1 ); + } + else + { + aRect.SetLeft( 0 ); + aRect.SetRight( aSize.Width()-1 ); + } + return aRect.Contains( rPos ); +} + +bool Slider::ImplIsPageDown( const Point& rPos ) const +{ + Size aSize = GetOutputSizePixel(); + tools::Rectangle aRect = maChannel2Rect; + if ( GetStyle() & WB_HORZ ) + { + aRect.SetTop( 0 ); + aRect.SetBottom( aSize.Height()-1 ); + } + else + { + aRect.SetLeft( 0 ); + aRect.SetRight( aSize.Width()-1 ); + } + return aRect.Contains( rPos ); +} + +tools::Long Slider::ImplSlide( tools::Long nNewPos ) +{ + tools::Long nOldPos = mnThumbPos; + SetThumbPos( nNewPos ); + tools::Long nDelta = mnThumbPos-nOldPos; + if ( nDelta ) + { + Slide(); + } + return nDelta; +} + +tools::Long Slider::ImplDoAction() +{ + tools::Long nDelta = 0; + + switch ( meScrollType ) + { + case ScrollType::LineUp: + nDelta = ImplSlide( mnThumbPos-mnLineSize ); + break; + + case ScrollType::LineDown: + nDelta = ImplSlide( mnThumbPos+mnLineSize ); + break; + + case ScrollType::PageUp: + nDelta = ImplSlide( mnThumbPos-mnPageSize ); + break; + + case ScrollType::PageDown: + nDelta = ImplSlide( mnThumbPos+mnPageSize ); + break; + + default: + break; + } + + return nDelta; +} + +void Slider::ImplDoMouseAction( const Point& rMousePos, bool bCallAction ) +{ + sal_uInt16 nOldStateFlags = mnStateFlags; + bool bAction = false; + + switch ( meScrollType ) + { + case ScrollType::PageUp: + if ( ImplIsPageUp( rMousePos ) ) + { + bAction = bCallAction; + mnStateFlags |= SLIDER_STATE_CHANNEL1_DOWN; + } + else + mnStateFlags &= ~SLIDER_STATE_CHANNEL1_DOWN; + break; + + case ScrollType::PageDown: + if ( ImplIsPageDown( rMousePos ) ) + { + bAction = bCallAction; + mnStateFlags |= SLIDER_STATE_CHANNEL2_DOWN; + } + else + mnStateFlags &= ~SLIDER_STATE_CHANNEL2_DOWN; + break; + default: + break; + } + + if ( bAction ) + { + if ( ImplDoAction() ) + { + Invalidate(); + } + } + else if ( nOldStateFlags != mnStateFlags ) + { + Invalidate(); + } +} + +void Slider::ImplDoSlide( tools::Long nNewPos ) +{ + if ( meScrollType != ScrollType::DontKnow ) + return; + + meScrollType = ScrollType::Drag; + ImplSlide( nNewPos ); + meScrollType = ScrollType::DontKnow; +} + +void Slider::ImplDoSlideAction( ScrollType eScrollType ) +{ + if ( (meScrollType != ScrollType::DontKnow) || + (eScrollType == ScrollType::DontKnow) || + (eScrollType == ScrollType::Drag) ) + return; + + meScrollType = eScrollType; + ImplDoAction(); + meScrollType = ScrollType::DontKnow; +} + +void Slider::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if ( !rMEvt.IsLeft() ) + return; + + const Point& rMousePos = rMEvt.GetPosPixel(); + StartTrackingFlags nTrackFlags = StartTrackingFlags::NONE; + + if ( maThumbRect.Contains( rMousePos ) ) + { + meScrollType = ScrollType::Drag; + + // calculate additional values + Point aCenterPos = maThumbRect.Center(); + if ( GetStyle() & WB_HORZ ) + mnMouseOff = rMousePos.X()-aCenterPos.X(); + else + mnMouseOff = rMousePos.Y()-aCenterPos.Y(); + } + else if ( ImplIsPageUp( rMousePos ) ) + { + nTrackFlags = StartTrackingFlags::ButtonRepeat; + meScrollType = ScrollType::PageUp; + } + else if ( ImplIsPageDown( rMousePos ) ) + { + nTrackFlags = StartTrackingFlags::ButtonRepeat; + meScrollType = ScrollType::PageDown; + } + + // Shall we start Tracking? + if( meScrollType != ScrollType::DontKnow ) + { + // store Start position for cancel and EndScroll delta + mnStartPos = mnThumbPos; + ImplDoMouseAction( rMousePos, /*bCallAction*/true ); + PaintImmediately(); + + StartTracking( nTrackFlags ); + } +} + +void Slider::MouseButtonUp( const MouseEvent& ) +{ +} + +void Slider::Tracking( const TrackingEvent& rTEvt ) +{ + if ( rTEvt.IsTrackingEnded() ) + { + // reset Button and PageRect state + sal_uInt16 nOldStateFlags = mnStateFlags; + mnStateFlags &= ~(SLIDER_STATE_CHANNEL1_DOWN | SLIDER_STATE_CHANNEL2_DOWN | + SLIDER_STATE_THUMB_DOWN); + if ( nOldStateFlags != mnStateFlags ) + { + Invalidate(InvalidateFlags::NoChildren | InvalidateFlags::NoErase); + } + + // on cancel, reset the previous Thumb position + if ( rTEvt.IsTrackingCanceled() ) + { + SetThumbPos( mnStartPos ); + Slide(); + } + + if ( meScrollType == ScrollType::Drag ) + { + // after dragging, recalculate to a rounded Thumb position + ImplCalc(); + PaintImmediately(); + } + + meScrollType = ScrollType::DontKnow; + } + else + { + const Point rMousePos = rTEvt.GetMouseEvent().GetPosPixel(); + + // special handling for dragging + if ( meScrollType == ScrollType::Drag ) + { + tools::Long nMovePix; + Point aCenterPos = maThumbRect.Center(); + if ( GetStyle() & WB_HORZ ) + nMovePix = rMousePos.X()-(aCenterPos.X()+mnMouseOff); + else + nMovePix = rMousePos.Y()-(aCenterPos.Y()+mnMouseOff); + // only if the mouse moves in Scroll direction we have to act + if ( nMovePix ) + { + mnThumbPixPos += nMovePix; + if ( mnThumbPixPos < mnThumbPixOffset ) + mnThumbPixPos = mnThumbPixOffset; + if ( mnThumbPixPos > (mnThumbPixOffset+mnThumbPixRange-1) ) + mnThumbPixPos = mnThumbPixOffset+mnThumbPixRange-1; + tools::Long nOldPos = mnThumbPos; + mnThumbPos = ImplCalcThumbPos( mnThumbPixPos ); + if ( nOldPos != mnThumbPos ) + { + ImplUpdateRects(); + PaintImmediately(); + if ( nOldPos != mnThumbPos ) + { + Slide(); + } + } + } + } + else + ImplDoMouseAction( rMousePos, rTEvt.IsTrackingRepeat() ); + + // end tracking if ScrollBar values indicate we are done + if ( !IsVisible() ) + EndTracking(); + } +} + +void Slider::KeyInput( const KeyEvent& rKEvt ) +{ + if ( !rKEvt.GetKeyCode().GetModifier() ) + { + switch ( rKEvt.GetKeyCode().GetCode() ) + { + case KEY_HOME: + ImplDoSlide( GetRangeMin() ); + break; + case KEY_END: + ImplDoSlide( GetRangeMax() ); + break; + + case KEY_LEFT: + case KEY_UP: + ImplDoSlideAction( ScrollType::LineUp ); + break; + + case KEY_RIGHT: + case KEY_DOWN: + ImplDoSlideAction( ScrollType::LineDown ); + break; + + case KEY_PAGEUP: + ImplDoSlideAction( ScrollType::PageUp ); + break; + + case KEY_PAGEDOWN: + ImplDoSlideAction( ScrollType::PageDown ); + break; + + default: + Control::KeyInput( rKEvt ); + break; + } + } + else + Control::KeyInput( rKEvt ); +} + +void Slider::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& /*rRect*/) +{ + ImplDraw(rRenderContext); +} + +void Slider::Resize() +{ + Control::Resize(); + mbCalcSize = true; + if ( IsReallyVisible() ) + ImplCalc( false ); + Invalidate(InvalidateFlags::NoChildren | InvalidateFlags::NoErase); +} + +void Slider::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( nType == StateChangedType::InitShow ) + ImplCalc( false ); + else if ( nType == StateChangedType::Data ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + ImplCalc(); + } + else if ( nType == StateChangedType::UpdateMode ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + { + ImplCalc( false ); + Invalidate(); + } + } + else if ( nType == StateChangedType::Enable ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + { + Invalidate(); + } + } + else if ( nType == StateChangedType::Style ) + { + if ( IsReallyVisible() && IsUpdateMode() ) + { + if ( (GetPrevStyle() & SLIDER_VIEW_STYLE) != + (GetStyle() & SLIDER_VIEW_STYLE) ) + { + mbCalcSize = true; + ImplCalc( false ); + Invalidate(); + } + } + } + else if ( nType == StateChangedType::ControlBackground ) + { + ImplInitSettings(); + Invalidate(); + } +} + +void Slider::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) ) + { + ImplInitSettings(); + Invalidate(); + } +} + +void Slider::Slide() +{ + maSlideHdl.Call( this ); +} + +void Slider::SetRangeMin(tools::Long nNewRange) +{ + SetRange(Range(nNewRange, GetRangeMax())); +} + +void Slider::SetRangeMax(tools::Long nNewRange) +{ + SetRange(Range(GetRangeMin(), nNewRange)); +} + +void Slider::SetRange( const Range& rRange ) +{ + // adjust Range + Range aRange = rRange; + aRange.Justify(); + tools::Long nNewMinRange = aRange.Min(); + tools::Long nNewMaxRange = aRange.Max(); + + // reset Range if different + if ( (mnMinRange != nNewMinRange) || + (mnMaxRange != nNewMaxRange) ) + { + mnMinRange = nNewMinRange; + mnMaxRange = nNewMaxRange; + + // adjust Thumb + if ( mnThumbPos > mnMaxRange ) + mnThumbPos = mnMaxRange; + if ( mnThumbPos < mnMinRange ) + mnThumbPos = mnMinRange; + CompatStateChanged( StateChangedType::Data ); + } +} + +void Slider::SetThumbPos( tools::Long nNewThumbPos ) +{ + if ( nNewThumbPos < mnMinRange ) + nNewThumbPos = mnMinRange; + if ( nNewThumbPos > mnMaxRange ) + nNewThumbPos = mnMaxRange; + + if ( mnThumbPos != nNewThumbPos ) + { + mnThumbPos = nNewThumbPos; + CompatStateChanged( StateChangedType::Data ); + } +} + +Size Slider::CalcWindowSizePixel() const +{ + tools::Long nWidth = mnMaxRange - mnMinRange + mnThumbSize + 1; + tools::Long nHeight = SLIDER_HEIGHT; + Size aSize; + if ( GetStyle() & WB_HORZ ) + { + aSize.setWidth( nWidth ); + aSize.setHeight( nHeight ); + } + else + { + aSize.setHeight( nWidth ); + aSize.setWidth( nHeight ); + } + return aSize; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/spinbtn.cxx b/vcl/source/control/spinbtn.cxx new file mode 100644 index 000000000..c800fdc7d --- /dev/null +++ b/vcl/source/control/spinbtn.cxx @@ -0,0 +1,468 @@ +/* -*- 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 <vcl/event.hxx> +#include <vcl/toolkit/spin.hxx> +#include <vcl/settings.hxx> +#include <vcl/vclevent.hxx> + +#include <spin.hxx> + +void SpinButton::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + mbUpperIn = false; + mbLowerIn = false; + mbInitialUp = false; + mbInitialDown = false; + + mnMinRange = 0; + mnMaxRange = 100; + mnValue = 0; + mnValueStep = 1; + + maRepeatTimer.SetTimeout(MouseSettings::GetButtonStartRepeat()); + maRepeatTimer.SetInvokeHandler(LINK(this, SpinButton, ImplTimeout)); + + mbRepeat = 0 != (nStyle & WB_REPEAT); + + if (nStyle & WB_HSCROLL) + mbHorz = true; + else + mbHorz = false; + + Control::ImplInit( pParent, nStyle, nullptr ); +} + +SpinButton::SpinButton( vcl::Window* pParent, WinBits nStyle ) + : Control(WindowType::SPINBUTTON) + , maRepeatTimer("SpinButton maRepeatTimer") + , mbUpperIsFocused(false) +{ + ImplInit(pParent, nStyle); +} + +IMPL_LINK(SpinButton, ImplTimeout, Timer*, pTimer, void) +{ + if (pTimer->GetTimeout() == static_cast<sal_uInt64>(MouseSettings::GetButtonStartRepeat())) + { + pTimer->SetTimeout( GetSettings().GetMouseSettings().GetButtonRepeat() ); + pTimer->Start(); + } + else + { + if (mbInitialUp) + Up(); + else + Down(); + } +} + +void SpinButton::Up() +{ + if (ImplIsUpperEnabled()) + { + mnValue += mnValueStep; + CompatStateChanged(StateChangedType::Data); + + ImplMoveFocus(true); + } + + ImplCallEventListenersAndHandler(VclEventId::SpinbuttonUp, nullptr ); +} + +void SpinButton::Down() +{ + if (ImplIsLowerEnabled()) + { + mnValue -= mnValueStep; + CompatStateChanged(StateChangedType::Data); + + ImplMoveFocus(false); + } + + ImplCallEventListenersAndHandler(VclEventId::SpinbuttonDown, nullptr ); +} + +void SpinButton::Resize() +{ + Control::Resize(); + + Size aSize(GetOutputSizePixel()); + tools::Rectangle aRect(Point(), aSize); + if (mbHorz) + { + maLowerRect = tools::Rectangle(0, 0, aSize.Width() / 2, aSize.Height() - 1); + maUpperRect = tools::Rectangle(maLowerRect.TopRight(), aRect.BottomRight()); + } + else + { + maUpperRect = tools::Rectangle(0, 0, aSize.Width() - 1, aSize.Height() / 2); + maLowerRect = tools::Rectangle(maUpperRect.BottomLeft(), aRect.BottomRight()); + } + + ImplCalcFocusRect(ImplIsUpperEnabled() || !ImplIsLowerEnabled()); + + Invalidate(); +} + +void SpinButton::Draw(OutputDevice* pDev, const Point& rPos, SystemTextColorFlags nFlags) +{ + Point aPos = pDev->LogicToPixel(rPos); + Size aSize = GetSizePixel(); + + pDev->Push(); + pDev->SetMapMode(); + if ( !(nFlags & SystemTextColorFlags::Mono) ) + { + // DecoView uses the FaceColor... + AllSettings aSettings = pDev->GetSettings(); + StyleSettings aStyleSettings = aSettings.GetStyleSettings(); + if ( IsControlBackground() ) + aStyleSettings.SetFaceColor( GetControlBackground() ); + else + aStyleSettings.SetFaceColor( GetSettings().GetStyleSettings().GetFaceColor() ); + + aSettings.SetStyleSettings( aStyleSettings ); + pDev->SetSettings( aSettings ); + } + + tools::Rectangle aRect( Point( 0, 0 ), aSize ); + tools::Rectangle aLowerRect, aUpperRect; + if ( mbHorz ) + { + aLowerRect = tools::Rectangle( 0, 0, aSize.Width()/2, aSize.Height()-1 ); + aUpperRect = tools::Rectangle( aLowerRect.TopRight(), aRect.BottomRight() ); + } + else + { + aUpperRect = tools::Rectangle( 0, 0, aSize.Width()-1, aSize.Height()/2 ); + aLowerRect = tools::Rectangle( aUpperRect.BottomLeft(), aRect.BottomRight() ); + } + + aUpperRect += aPos; + aLowerRect += aPos; + + ImplDrawSpinButton(*pDev, this, aUpperRect, aLowerRect, false, false, + IsEnabled() && ImplIsUpperEnabled(), + IsEnabled() && ImplIsLowerEnabled(), mbHorz, true); + pDev->Pop(); +} + +void SpinButton::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& /*rRect*/) +{ + HideFocus(); + + bool bEnable = IsEnabled(); + ImplDrawSpinButton(rRenderContext, this, maUpperRect, maLowerRect, mbUpperIn, mbLowerIn, + bEnable && ImplIsUpperEnabled(), + bEnable && ImplIsLowerEnabled(), mbHorz, true); + + if (HasFocus()) + ShowFocus(maFocusRect); +} + +void SpinButton::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if ( maUpperRect.Contains( rMEvt.GetPosPixel() ) && ( ImplIsUpperEnabled() ) ) + { + mbUpperIn = true; + mbInitialUp = true; + Invalidate( maUpperRect ); + } + else if ( maLowerRect.Contains( rMEvt.GetPosPixel() ) && ( ImplIsLowerEnabled() ) ) + { + mbLowerIn = true; + mbInitialDown = true; + Invalidate( maLowerRect ); + } + + if ( mbUpperIn || mbLowerIn ) + { + CaptureMouse(); + if ( mbRepeat ) + maRepeatTimer.Start(); + } +} + +void SpinButton::MouseButtonUp( const MouseEvent& ) +{ + ReleaseMouse(); + if ( mbRepeat ) + { + maRepeatTimer.Stop(); + maRepeatTimer.SetTimeout(MouseSettings::GetButtonStartRepeat() ); + } + + if ( mbUpperIn ) + { + mbUpperIn = false; + Invalidate( maUpperRect ); + Up(); + } + else if ( mbLowerIn ) + { + mbLowerIn = false; + Invalidate( maLowerRect ); + Down(); + } + + mbInitialUp = mbInitialDown = false; +} + +void SpinButton::MouseMove( const MouseEvent& rMEvt ) +{ + if ( !rMEvt.IsLeft() || (!mbInitialUp && !mbInitialDown) ) + return; + + if ( !maUpperRect.Contains( rMEvt.GetPosPixel() ) && + mbUpperIn && mbInitialUp ) + { + mbUpperIn = false; + maRepeatTimer.Stop(); + Invalidate( maUpperRect ); + } + else if ( !maLowerRect.Contains( rMEvt.GetPosPixel() ) && + mbLowerIn && mbInitialDown ) + { + mbLowerIn = false; + maRepeatTimer.Stop(); + Invalidate( maLowerRect ); + } + else if ( maUpperRect.Contains( rMEvt.GetPosPixel() ) && + !mbUpperIn && mbInitialUp ) + { + mbUpperIn = true; + if ( mbRepeat ) + maRepeatTimer.Start(); + Invalidate( maUpperRect ); + } + else if ( maLowerRect.Contains( rMEvt.GetPosPixel() ) && + !mbLowerIn && mbInitialDown ) + { + mbLowerIn = true; + if ( mbRepeat ) + maRepeatTimer.Start(); + Invalidate( maLowerRect ); + } +} + +void SpinButton::KeyInput( const KeyEvent& rKEvt ) +{ + if ( !rKEvt.GetKeyCode().GetModifier() ) + { + switch ( rKEvt.GetKeyCode().GetCode() ) + { + case KEY_LEFT: + case KEY_RIGHT: + { + bool bUp = KEY_RIGHT == rKEvt.GetKeyCode().GetCode(); + if ( mbHorz && !ImplMoveFocus( bUp ) ) + bUp ? Up() : Down(); + } + break; + + case KEY_UP: + case KEY_DOWN: + { + bool bUp = KEY_UP == rKEvt.GetKeyCode().GetCode(); + if ( !mbHorz && !ImplMoveFocus( KEY_UP == rKEvt.GetKeyCode().GetCode() ) ) + bUp ? Up() : Down(); + } + break; + + case KEY_SPACE: + mbUpperIsFocused ? Up() : Down(); + break; + + default: + Control::KeyInput( rKEvt ); + break; + } + } + else + Control::KeyInput( rKEvt ); +} + +void SpinButton::StateChanged( StateChangedType nType ) +{ + switch ( nType ) + { + case StateChangedType::Data: + case StateChangedType::Enable: + Invalidate(); + break; + + case StateChangedType::Style: + { + bool bNewRepeat = 0 != ( GetStyle() & WB_REPEAT ); + if ( bNewRepeat != mbRepeat ) + { + if ( maRepeatTimer.IsActive() ) + { + maRepeatTimer.Stop(); + maRepeatTimer.SetTimeout( MouseSettings::GetButtonStartRepeat() ); + } + mbRepeat = bNewRepeat; + } + + bool bNewHorz = 0 != ( GetStyle() & WB_HSCROLL ); + if ( bNewHorz != mbHorz ) + { + mbHorz = bNewHorz; + Resize(); + } + } + break; + default:; + } + + Control::StateChanged( nType ); +} + +void SpinButton::SetRangeMin( tools::Long nNewRange ) +{ + SetRange( Range( nNewRange, GetRangeMax() ) ); +} + +void SpinButton::SetRangeMax( tools::Long nNewRange ) +{ + SetRange( Range( GetRangeMin(), nNewRange ) ); +} + +void SpinButton::SetRange( const Range& rRange ) +{ + // adjust rage + Range aRange = rRange; + aRange.Justify(); + tools::Long nNewMinRange = aRange.Min(); + tools::Long nNewMaxRange = aRange.Max(); + + // do something only if old and new range differ + if ( (mnMinRange == nNewMinRange) && (mnMaxRange == nNewMaxRange)) + return; + + mnMinRange = nNewMinRange; + mnMaxRange = nNewMaxRange; + + // adjust value to new range, if necessary + if ( mnValue > mnMaxRange ) + mnValue = mnMaxRange; + if ( mnValue < mnMinRange ) + mnValue = mnMinRange; + + CompatStateChanged( StateChangedType::Data ); +} + +void SpinButton::SetValue( tools::Long nValue ) +{ + // adjust, if necessary + if ( nValue > mnMaxRange ) + nValue = mnMaxRange; + if ( nValue < mnMinRange ) + nValue = mnMinRange; + + if ( mnValue != nValue ) + { + mnValue = nValue; + CompatStateChanged( StateChangedType::Data ); + } +} + +void SpinButton::GetFocus() +{ + ShowFocus( maFocusRect ); + Control::GetFocus(); +} + +void SpinButton::LoseFocus() +{ + HideFocus(); + Control::LoseFocus(); +} + +bool SpinButton::ImplMoveFocus( bool _bUpper ) +{ + if ( _bUpper == mbUpperIsFocused ) + return false; + + HideFocus(); + ImplCalcFocusRect( _bUpper ); + if ( HasFocus() ) + ShowFocus( maFocusRect ); + return true; +} + +void SpinButton::ImplCalcFocusRect( bool _bUpper ) +{ + maFocusRect = _bUpper ? maUpperRect : maLowerRect; + // inflate by some pixels + maFocusRect.AdjustLeft(2 ); + maFocusRect.AdjustTop(2 ); + maFocusRect.AdjustRight( -2 ); + maFocusRect.AdjustBottom( -2 ); + mbUpperIsFocused = _bUpper; +} + +tools::Rectangle* SpinButton::ImplFindPartRect( const Point& rPt ) +{ + if( maUpperRect.Contains( rPt ) ) + return &maUpperRect; + else if( maLowerRect.Contains( rPt ) ) + return &maLowerRect; + else + return nullptr; +} + +bool SpinButton::PreNotify( NotifyEvent& rNEvt ) +{ + if (rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if (pMouseEvt && !pMouseEvt->GetButtons() && !pMouseEvt->IsSynthetic() && !pMouseEvt->IsModifierChanged()) + { + // trigger redraw if mouse over state has changed + if (IsNativeControlSupported(ControlType::Spinbox, ControlPart::Entire) || + IsNativeControlSupported(ControlType::Spinbox, ControlPart::AllButtons) ) + { + tools::Rectangle* pRect = ImplFindPartRect( GetPointerPosPixel() ); + tools::Rectangle* pLastRect = ImplFindPartRect( GetLastPointerPosPixel() ); + if (pRect != pLastRect || (pMouseEvt->IsLeaveWindow() || pMouseEvt->IsEnterWindow())) + { + vcl::Region aRgn(GetOutDev()->GetActiveClipRegion()); + if (pLastRect) + { + GetOutDev()->SetClipRegion(vcl::Region(*pLastRect)); + Invalidate(*pLastRect); + GetOutDev()->SetClipRegion( aRgn ); + } + if (pRect) + { + GetOutDev()->SetClipRegion(vcl::Region(*pRect)); + Invalidate(*pRect); + GetOutDev()->SetClipRegion(aRgn); + } + } + } + } + } + + return Control::PreNotify(rNEvt); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/spinfld.cxx b/vcl/source/control/spinfld.cxx new file mode 100644 index 000000000..6bc1fac29 --- /dev/null +++ b/vcl/source/control/spinfld.cxx @@ -0,0 +1,1034 @@ +/* -*- 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 <vcl/commandevent.hxx> +#include <vcl/event.hxx> +#include <vcl/decoview.hxx> +#include <vcl/toolkit/spinfld.hxx> +#include <vcl/settings.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <sal/log.hxx> + +#include <spin.hxx> +#include <svdata.hxx> + +namespace { + +void ImplGetSpinbuttonValue(vcl::Window* pWin, + const tools::Rectangle& rUpperRect, const tools::Rectangle& rLowerRect, + bool bUpperIn, bool bLowerIn, bool bUpperEnabled, bool bLowerEnabled, + bool bHorz, SpinbuttonValue& rValue ) +{ + // convert spinbutton data to a SpinbuttonValue structure for native painting + + rValue.maUpperRect = rUpperRect; + rValue.maLowerRect = rLowerRect; + + Point aPointerPos = pWin->GetPointerPosPixel(); + + ControlState nState = ControlState::ENABLED; + if (bUpperIn) + nState |= ControlState::PRESSED; + if (!pWin->IsEnabled() || !bUpperEnabled) + nState &= ~ControlState::ENABLED; + if (pWin->HasFocus()) + nState |= ControlState::FOCUSED; + if (pWin->IsMouseOver() && rUpperRect.Contains(aPointerPos)) + nState |= ControlState::ROLLOVER; + rValue.mnUpperState = nState; + + nState = ControlState::ENABLED; + if (bLowerIn) + nState |= ControlState::PRESSED; + if (!pWin->IsEnabled() || !bLowerEnabled) + nState &= ~ControlState::ENABLED; + if (pWin->HasFocus()) + nState |= ControlState::FOCUSED; + // for overlapping spins: highlight only one + if (pWin->IsMouseOver() && rLowerRect.Contains(aPointerPos) && !rUpperRect.Contains(aPointerPos)) + nState |= ControlState::ROLLOVER; + rValue.mnLowerState = nState; + + rValue.mnUpperPart = bHorz ? ControlPart::ButtonLeft : ControlPart::ButtonUp; + rValue.mnLowerPart = bHorz ? ControlPart::ButtonRight : ControlPart::ButtonDown; +} + +bool ImplDrawNativeSpinfield(vcl::RenderContext& rRenderContext, vcl::Window const * pWin, const SpinbuttonValue& rSpinbuttonValue) +{ + bool bNativeOK = false; + + if (rRenderContext.IsNativeControlSupported(ControlType::Spinbox, ControlPart::Entire) && + // there is just no useful native support for spinfields with dropdown + !(pWin->GetStyle() & WB_DROPDOWN)) + { + if (rRenderContext.IsNativeControlSupported(ControlType::Spinbox, rSpinbuttonValue.mnUpperPart) && + rRenderContext.IsNativeControlSupported(ControlType::Spinbox, rSpinbuttonValue.mnLowerPart)) + { + // only paint the embedded spin buttons, all buttons are painted at once + tools::Rectangle aUpperAndLowerButtons( rSpinbuttonValue.maUpperRect.GetUnion( rSpinbuttonValue.maLowerRect ) ); + bNativeOK = rRenderContext.DrawNativeControl(ControlType::Spinbox, ControlPart::AllButtons, aUpperAndLowerButtons, + ControlState::ENABLED, rSpinbuttonValue, OUString()); + } + else + { + // paint the spinbox as a whole, use borderwindow to have proper clipping + vcl::Window* pBorder = pWin->GetWindow(GetWindowType::Border); + + // to not overwrite everything, set the button region as clipregion to the border window + tools::Rectangle aClipRect(rSpinbuttonValue.maLowerRect); + aClipRect.Union(rSpinbuttonValue.maUpperRect); + + vcl::RenderContext* pContext = &rRenderContext; + vcl::Region oldRgn; + Point aPt; + Size aSize(pBorder->GetOutputSizePixel()); // the size of the border window, i.e., the whole control + tools::Rectangle aNatRgn(aPt, aSize); + + if (!pWin->SupportsDoubleBuffering()) + { + // convert from screen space to borderwin space + aClipRect.SetPos(pBorder->ScreenToOutputPixel(pWin->OutputToScreenPixel(aClipRect.TopLeft()))); + + oldRgn = pBorder->GetOutDev()->GetClipRegion(); + pBorder->GetOutDev()->SetClipRegion(vcl::Region(aClipRect)); + + pContext = pBorder->GetOutDev(); + } + + tools::Rectangle aBound, aContent; + if (!ImplGetSVData()->maNWFData.mbCanDrawWidgetAnySize && + pContext->GetNativeControlRegion(ControlType::Spinbox, ControlPart::Entire, + aNatRgn, ControlState::NONE, rSpinbuttonValue, + aBound, aContent)) + { + aSize = aContent.GetSize(); + } + + tools::Rectangle aRgn(aPt, aSize); + if (pWin->SupportsDoubleBuffering()) + { + // convert from borderwin space, to the pWin's space + aRgn.SetPos(pWin->ScreenToOutputPixel(pBorder->OutputToScreenPixel(aRgn.TopLeft()))); + } + + bNativeOK = pContext->DrawNativeControl(ControlType::Spinbox, ControlPart::Entire, aRgn, + ControlState::ENABLED, rSpinbuttonValue, OUString()); + + if (!pWin->SupportsDoubleBuffering()) + pBorder->GetOutDev()->SetClipRegion(oldRgn); + } + } + return bNativeOK; +} + +bool ImplDrawNativeSpinbuttons(vcl::RenderContext& rRenderContext, const SpinbuttonValue& rSpinbuttonValue) +{ + bool bNativeOK = false; + + if (rRenderContext.IsNativeControlSupported(ControlType::SpinButtons, ControlPart::Entire)) + { + tools::Rectangle aArea = rSpinbuttonValue.maUpperRect.GetUnion(rSpinbuttonValue.maLowerRect); + // only paint the standalone spin buttons, all buttons are painted at once + bNativeOK = rRenderContext.DrawNativeControl(ControlType::SpinButtons, ControlPart::AllButtons, aArea, + ControlState::ENABLED, rSpinbuttonValue, OUString()); + } + return bNativeOK; +} + +} + +void ImplDrawSpinButton(vcl::RenderContext& rRenderContext, vcl::Window* pWindow, + const tools::Rectangle& rUpperRect, const tools::Rectangle& rLowerRect, + bool bUpperIn, bool bLowerIn, bool bUpperEnabled, bool bLowerEnabled, + bool bHorz, bool bMirrorHorz) +{ + bool bNativeOK = false; + + if (pWindow) + { + // are we drawing standalone spin buttons or members of a spinfield ? + ControlType aControl = ControlType::SpinButtons; + switch (pWindow->GetType()) + { + case WindowType::EDIT: + case WindowType::MULTILINEEDIT: + case WindowType::PATTERNFIELD: + case WindowType::METRICFIELD: + case WindowType::CURRENCYFIELD: + case WindowType::DATEFIELD: + case WindowType::TIMEFIELD: + case WindowType::SPINFIELD: + case WindowType::FORMATTEDFIELD: + aControl = ControlType::Spinbox; + break; + default: + aControl = ControlType::SpinButtons; + break; + } + + SpinbuttonValue aValue; + ImplGetSpinbuttonValue(pWindow, rUpperRect, rLowerRect, + bUpperIn, bLowerIn, bUpperEnabled, bLowerEnabled, + bHorz, aValue); + + if( aControl == ControlType::Spinbox ) + bNativeOK = ImplDrawNativeSpinfield(rRenderContext, pWindow, aValue); + else if( aControl == ControlType::SpinButtons ) + bNativeOK = ImplDrawNativeSpinbuttons(rRenderContext, aValue); + } + + if (bNativeOK) + return; + + ImplDrawUpDownButtons(rRenderContext, + rUpperRect, rLowerRect, + bUpperIn, bLowerIn, bUpperEnabled, bLowerEnabled, + bHorz, bMirrorHorz); +} + +void ImplDrawUpDownButtons(vcl::RenderContext& rRenderContext, + const tools::Rectangle& rUpperRect, const tools::Rectangle& rLowerRect, + bool bUpperIn, bool bLowerIn, bool bUpperEnabled, bool bLowerEnabled, + bool bHorz, bool bMirrorHorz) +{ + DecorationView aDecoView(&rRenderContext); + + SymbolType eType1, eType2; + + if ( bHorz ) + { + eType1 = bMirrorHorz ? SymbolType::SPIN_RIGHT : SymbolType::SPIN_LEFT; + eType2 = bMirrorHorz ? SymbolType::SPIN_LEFT : SymbolType::SPIN_RIGHT; + } + else + { + eType1 = SymbolType::SPIN_UP; + eType2 = SymbolType::SPIN_DOWN; + } + + DrawButtonFlags nStyle = DrawButtonFlags::NoLeftLightBorder; + // draw upper/left Button + if (bUpperIn) + nStyle |= DrawButtonFlags::Pressed; + + tools::Rectangle aUpRect = aDecoView.DrawButton(rUpperRect, nStyle); + + nStyle = DrawButtonFlags::NoLeftLightBorder; + // draw lower/right Button + if (bLowerIn) + nStyle |= DrawButtonFlags::Pressed; + + tools::Rectangle aLowRect = aDecoView.DrawButton(rLowerRect, nStyle); + + // make use of additional default edge + aUpRect.AdjustLeft( -1 ); + aUpRect.AdjustTop( -1 ); + aUpRect.AdjustRight( 1 ); + aUpRect.AdjustBottom( 1 ); + aLowRect.AdjustLeft( -1 ); + aLowRect.AdjustTop( -1 ); + aLowRect.AdjustRight( 1 ); + aLowRect.AdjustBottom( 1 ); + + // draw into the edge, so that something is visible if the rectangle is too small + if (aUpRect.GetHeight() < 4) + { + aUpRect.AdjustRight( 1 ); + aUpRect.AdjustBottom( 1 ); + aLowRect.AdjustRight( 1 ); + aLowRect.AdjustBottom( 1 ); + } + + // calculate Symbol size + tools::Long nTempSize1 = aUpRect.GetWidth(); + tools::Long nTempSize2 = aLowRect.GetWidth(); + if (std::abs( nTempSize1-nTempSize2 ) == 1) + { + if (nTempSize1 > nTempSize2) + aUpRect.AdjustLeft( 1 ); + else + aLowRect.AdjustLeft( 1 ); + } + nTempSize1 = aUpRect.GetHeight(); + nTempSize2 = aLowRect.GetHeight(); + if (std::abs(nTempSize1 - nTempSize2) == 1) + { + if (nTempSize1 > nTempSize2) + aUpRect.AdjustTop( 1 ); + else + aLowRect.AdjustTop( 1 ); + } + + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + DrawSymbolFlags nSymStyle = DrawSymbolFlags::NONE; + if (!bUpperEnabled) + nSymStyle |= DrawSymbolFlags::Disable; + aDecoView.DrawSymbol(aUpRect, eType1, rStyleSettings.GetButtonTextColor(), nSymStyle); + + nSymStyle = DrawSymbolFlags::NONE; + if (!bLowerEnabled) + nSymStyle |= DrawSymbolFlags::Disable; + aDecoView.DrawSymbol(aLowRect, eType2, rStyleSettings.GetButtonTextColor(), nSymStyle); +} + +void SpinField::ImplInitSpinFieldData() +{ + mpEdit.disposeAndClear(); + mbSpin = false; + mbRepeat = false; + mbUpperIn = false; + mbLowerIn = false; + mbInitialUp = false; + mbInitialDown = false; + mbInDropDown = false; + mbUpperEnabled = true; + mbLowerEnabled = true; +} + +void SpinField::ImplInit(vcl::Window* pParent, WinBits nWinStyle) +{ + Edit::ImplInit( pParent, nWinStyle ); + + if (!(nWinStyle & (WB_SPIN | WB_DROPDOWN))) + return; + + mbSpin = true; + + // Some themes want external spin buttons, therefore the main + // spinfield should not overdraw the border between its encapsulated + // edit field and the spin buttons + if ((nWinStyle & WB_SPIN) && ImplUseNativeBorder(*GetOutDev(), nWinStyle)) + { + SetBackground(); + mpEdit.set(VclPtr<Edit>::Create(this, WB_NOBORDER)); + mpEdit->SetBackground(); + } + else + mpEdit.set(VclPtr<Edit>::Create(this, WB_NOBORDER)); + + mpEdit->EnableRTL(false); + mpEdit->SetPosPixel(Point()); + mpEdit->Show(); + + SetSubEdit(mpEdit); + + maRepeatTimer.SetInvokeHandler(LINK( this, SpinField, ImplTimeout)); + maRepeatTimer.SetTimeout(MouseSettings::GetButtonStartRepeat()); + if (nWinStyle & WB_REPEAT) + mbRepeat = true; + + SetCompoundControl(true); +} + +SpinField::SpinField(vcl::Window* pParent, WinBits nWinStyle, WindowType nType) : + Edit(nType), maRepeatTimer("SpinField maRepeatTimer") +{ + ImplInitSpinFieldData(); + ImplInit(pParent, nWinStyle); +} + +SpinField::~SpinField() +{ + disposeOnce(); +} + +void SpinField::dispose() +{ + mpEdit.disposeAndClear(); + + Edit::dispose(); +} + +void SpinField::Up() +{ + ImplCallEventListenersAndHandler( VclEventId::SpinfieldUp, [this] () { maUpHdlLink.Call(*this); } ); +} + +void SpinField::Down() +{ + ImplCallEventListenersAndHandler( VclEventId::SpinfieldDown, [this] () { maDownHdlLink.Call(*this); } ); +} + +void SpinField::First() +{ + ImplCallEventListenersAndHandler(VclEventId::SpinfieldFirst, nullptr); +} + +void SpinField::Last() +{ + ImplCallEventListenersAndHandler(VclEventId::SpinfieldLast, nullptr); +} + +void SpinField::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if (!HasFocus() && (!mpEdit || !mpEdit->HasFocus())) + { + GrabFocus(); + } + + if (!IsReadOnly()) + { + if (maUpperRect.Contains(rMEvt.GetPosPixel())) + { + mbUpperIn = true; + mbInitialUp = true; + Invalidate(maUpperRect); + } + else if (maLowerRect.Contains(rMEvt.GetPosPixel())) + { + mbLowerIn = true; + mbInitialDown = true; + Invalidate(maLowerRect); + } + else if (maDropDownRect.Contains(rMEvt.GetPosPixel())) + { + // put DropDownButton to the right + mbInDropDown = ShowDropDown( !mbInDropDown ); + Invalidate(tools::Rectangle(Point(), GetOutputSizePixel())); + } + + if (mbUpperIn || mbLowerIn) + { + CaptureMouse(); + if (mbRepeat) + maRepeatTimer.Start(); + return; + } + } + + Edit::MouseButtonDown(rMEvt); +} + +void SpinField::MouseButtonUp(const MouseEvent& rMEvt) +{ + ReleaseMouse(); + mbInitialUp = mbInitialDown = false; + maRepeatTimer.Stop(); + maRepeatTimer.SetTimeout(MouseSettings::GetButtonStartRepeat()); + + if (mbUpperIn) + { + mbUpperIn = false; + Invalidate(maUpperRect); + Up(); + } + else if (mbLowerIn) + { + mbLowerIn = false; + Invalidate(maLowerRect); + Down(); + } + + Edit::MouseButtonUp(rMEvt); +} + +void SpinField::MouseMove(const MouseEvent& rMEvt) +{ + if (rMEvt.IsLeft()) + { + if (mbInitialUp) + { + bool bNewUpperIn = maUpperRect.Contains(rMEvt.GetPosPixel()); + if (bNewUpperIn != mbUpperIn) + { + if (bNewUpperIn) + { + if (mbRepeat) + maRepeatTimer.Start(); + } + else + maRepeatTimer.Stop(); + + mbUpperIn = bNewUpperIn; + Invalidate(maUpperRect); + } + } + else if (mbInitialDown) + { + bool bNewLowerIn = maLowerRect.Contains(rMEvt.GetPosPixel()); + if (bNewLowerIn != mbLowerIn) + { + if (bNewLowerIn) + { + if (mbRepeat) + maRepeatTimer.Start(); + } + else + maRepeatTimer.Stop(); + + mbLowerIn = bNewLowerIn; + Invalidate(maLowerRect); + } + } + } + + Edit::MouseMove(rMEvt); +} + +bool SpinField::EventNotify(NotifyEvent& rNEvt) +{ + bool bDone = false; + if (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) + { + const KeyEvent& rKEvt = *rNEvt.GetKeyEvent(); + if (!IsReadOnly()) + { + sal_uInt16 nMod = rKEvt.GetKeyCode().GetModifier(); + switch (rKEvt.GetKeyCode().GetCode()) + { + case KEY_UP: + { + if (!nMod) + { + Up(); + bDone = true; + } + } + break; + case KEY_DOWN: + { + if (!nMod) + { + Down(); + bDone = true; + } + else if ((nMod == KEY_MOD2) && !mbInDropDown && (GetStyle() & WB_DROPDOWN)) + { + mbInDropDown = ShowDropDown(true); + Invalidate(tools::Rectangle(Point(), GetOutputSizePixel())); + bDone = true; + } + } + break; + case KEY_PAGEUP: + { + if (!nMod) + { + Last(); + bDone = true; + } + } + break; + case KEY_PAGEDOWN: + { + if (!nMod) + { + First(); + bDone = true; + } + } + break; + } + } + } + + if (rNEvt.GetType() == MouseNotifyEvent::COMMAND) + { + if ((rNEvt.GetCommandEvent()->GetCommand() == CommandEventId::Wheel) && !IsReadOnly()) + { + MouseWheelBehaviour nWheelBehavior(GetSettings().GetMouseSettings().GetWheelBehavior()); + if (nWheelBehavior == MouseWheelBehaviour::ALWAYS + || (nWheelBehavior == MouseWheelBehaviour::FocusOnly && HasChildPathFocus())) + { + const CommandWheelData* pData = rNEvt.GetCommandEvent()->GetWheelData(); + if (pData->GetMode() == CommandWheelMode::SCROLL) + { + if (pData->GetDelta() < 0) + Down(); + else + Up(); + bDone = true; + + if (!HasChildPathFocus()) + GrabFocus(); + } + } + else + bDone = false; // don't eat this event, let the default handling happen (i.e. scroll the context) + } + } + + return bDone || Edit::EventNotify(rNEvt); +} + +void SpinField::FillLayoutData() const +{ + if (mbSpin) + { + mxLayoutData.emplace(); + AppendLayoutData(*GetSubEdit()); + GetSubEdit()->SetLayoutDataParent(this); + } + else + Edit::FillLayoutData(); +} + +void SpinField::SetUpperEnabled(bool bEnabled) +{ + if (mbUpperEnabled == bEnabled) + return; + + mbUpperEnabled = bEnabled; + + if (mbSpin) + Invalidate(maUpperRect); +} + +void SpinField::SetLowerEnabled(bool bEnabled) +{ + if (mbLowerEnabled == bEnabled) + return; + + mbLowerEnabled = bEnabled; + + if (mbSpin) + Invalidate(maLowerRect); +} + +void SpinField::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + if (mbSpin) + { + bool bEnabled = IsEnabled(); + bool bUpperEnabled = bEnabled && IsUpperEnabled(); + bool bLowerEnabled = bEnabled && IsLowerEnabled(); + ImplDrawSpinButton(rRenderContext, this, maUpperRect, maLowerRect, + mbUpperIn && bUpperEnabled, mbLowerIn && bLowerEnabled, + bUpperEnabled, bLowerEnabled); + } + + if (GetStyle() & WB_DROPDOWN) + { + DecorationView aView(&rRenderContext); + + DrawButtonFlags nStyle = DrawButtonFlags::NoLightBorder; + if (mbInDropDown) + nStyle |= DrawButtonFlags::Pressed; + tools::Rectangle aInnerRect = aView.DrawButton(maDropDownRect, nStyle); + + DrawSymbolFlags nSymbolStyle = IsEnabled() ? DrawSymbolFlags::NONE : DrawSymbolFlags::Disable; + aView.DrawSymbol(aInnerRect, SymbolType::SPIN_DOWN, rRenderContext.GetSettings().GetStyleSettings().GetButtonTextColor(), nSymbolStyle); + } + + Edit::Paint(rRenderContext, rRect); +} + +void SpinField::ImplCalcButtonAreas(const OutputDevice* pDev, const Size& rOutSz, tools::Rectangle& rDDArea, + tools::Rectangle& rSpinUpArea, tools::Rectangle& rSpinDownArea) +{ + const StyleSettings& rStyleSettings = pDev->GetSettings().GetStyleSettings(); + + Size aSize = rOutSz; + Size aDropDownSize; + + if (GetStyle() & WB_DROPDOWN) + { + tools::Long nW = rStyleSettings.GetScrollBarSize(); + nW = GetDrawPixel( pDev, nW ); + aDropDownSize = Size( CalcZoom( nW ), aSize.Height() ); + aSize.AdjustWidth( -(aDropDownSize.Width()) ); + rDDArea = tools::Rectangle( Point( aSize.Width(), 0 ), aDropDownSize ); + rDDArea.AdjustTop( -1 ); + } + else + rDDArea.SetEmpty(); + + // calculate sizes according to the height + if (GetStyle() & WB_SPIN) + { + tools::Long nBottom1 = aSize.Height()/2; + tools::Long nBottom2 = aSize.Height()-1; + tools::Long nTop2 = nBottom1; + if ( !(aSize.Height() & 0x01) ) + nBottom1--; + + bool bNativeRegionOK = false; + tools::Rectangle aContentUp, aContentDown; + + if ((pDev->GetOutDevType() == OUTDEV_WINDOW) && + // there is just no useful native support for spinfields with dropdown + ! (GetStyle() & WB_DROPDOWN) && + IsNativeControlSupported(ControlType::Spinbox, ControlPart::Entire)) + { + vcl::Window *pWin = pDev->GetOwnerWindow(); + vcl::Window *pBorder = pWin->GetWindow( GetWindowType::Border ); + + // get the system's spin button size + ImplControlValue aControlValue; + tools::Rectangle aBound; + Point aPoint; + + // use the full extent of the control + tools::Rectangle aArea( aPoint, pBorder->GetOutputSizePixel() ); + + bNativeRegionOK = + pWin->GetNativeControlRegion(ControlType::Spinbox, ControlPart::ButtonUp, + aArea, ControlState::NONE, aControlValue, aBound, aContentUp) && + pWin->GetNativeControlRegion(ControlType::Spinbox, ControlPart::ButtonDown, + aArea, ControlState::NONE, aControlValue, aBound, aContentDown); + + if (bNativeRegionOK) + { + // convert back from border space to local coordinates + aPoint = pBorder->ScreenToOutputPixel( pWin->OutputToScreenPixel( aPoint ) ); + aContentUp.Move(-aPoint.X(), -aPoint.Y()); + aContentDown.Move(-aPoint.X(), -aPoint.Y()); + } + } + + if (bNativeRegionOK) + { + rSpinUpArea = aContentUp; + rSpinDownArea = aContentDown; + } + else + { + aSize.AdjustWidth( -(CalcZoom( GetDrawPixel( pDev, rStyleSettings.GetSpinSize() ) )) ); + + rSpinUpArea = tools::Rectangle( aSize.Width(), 0, rOutSz.Width()-aDropDownSize.Width()-1, nBottom1 ); + rSpinDownArea = tools::Rectangle( rSpinUpArea.Left(), nTop2, rSpinUpArea.Right(), nBottom2 ); + } + } + else + { + rSpinUpArea.SetEmpty(); + rSpinDownArea.SetEmpty(); + } +} + +void SpinField::Resize() +{ + if (!mbSpin) + return; + + Control::Resize(); + Size aSize = GetOutputSizePixel(); + bool bSubEditPositioned = false; + + if (GetStyle() & (WB_SPIN | WB_DROPDOWN)) + { + ImplCalcButtonAreas( GetOutDev(), aSize, maDropDownRect, maUpperRect, maLowerRect ); + + ImplControlValue aControlValue; + Point aPoint; + tools::Rectangle aContent, aBound; + + // use the full extent of the control + vcl::Window *pBorder = GetWindow( GetWindowType::Border ); + tools::Rectangle aArea( aPoint, pBorder->GetOutputSizePixel() ); + + // adjust position and size of the edit field + if (GetNativeControlRegion(ControlType::Spinbox, ControlPart::SubEdit, aArea, ControlState::NONE, + aControlValue, aBound, aContent) && + // there is just no useful native support for spinfields with dropdown + !(GetStyle() & WB_DROPDOWN)) + { + // convert back from border space to local coordinates + aPoint = pBorder->ScreenToOutputPixel(OutputToScreenPixel(aPoint)); + aContent.Move(-aPoint.X(), -aPoint.Y()); + + // use the themes drop down size + mpEdit->SetPosPixel( aContent.TopLeft() ); + bSubEditPositioned = true; + aSize = aContent.GetSize(); + } + else + { + if (maUpperRect.IsEmpty()) + { + SAL_WARN_IF( maDropDownRect.IsEmpty(), "vcl", "SpinField::Resize: SPIN && DROPDOWN, but all empty rects?" ); + aSize.setWidth( maDropDownRect.Left() ); + } + else + aSize.setWidth( maUpperRect.Left() ); + } + } + + if (!bSubEditPositioned) + { + // this moves our sub edit if RTL gets switched + mpEdit->SetPosPixel(Point()); + } + mpEdit->SetSizePixel(aSize); + + if (GetStyle() & WB_SPIN) + Invalidate(tools::Rectangle(maUpperRect.TopLeft(), maLowerRect.BottomRight())); + if (GetStyle() & WB_DROPDOWN) + Invalidate(maDropDownRect); +} + +void SpinField::StateChanged(StateChangedType nType) +{ + Edit::StateChanged(nType); + + if (nType == StateChangedType::Enable) + { + if (mbSpin || (GetStyle() & WB_DROPDOWN)) + { + mpEdit->Enable(IsEnabled()); + + if (mbSpin) + { + Invalidate(maLowerRect); + Invalidate(maUpperRect); + } + if (GetStyle() & WB_DROPDOWN) + Invalidate(maDropDownRect); + } + } + else if (nType == StateChangedType::Style) + { + if (GetStyle() & WB_REPEAT) + mbRepeat = true; + else + mbRepeat = false; + } + else if (nType == StateChangedType::Zoom) + { + Resize(); + if (mpEdit) + mpEdit->SetZoom(GetZoom()); + Invalidate(); + } + else if (nType == StateChangedType::ControlFont) + { + if (mpEdit) + mpEdit->SetControlFont(GetControlFont()); + Invalidate(); + } + else if (nType == StateChangedType::ControlForeground) + { + if (mpEdit) + mpEdit->SetControlForeground(GetControlForeground()); + Invalidate(); + } + else if (nType == StateChangedType::ControlBackground) + { + if (mpEdit) + mpEdit->SetControlBackground(GetControlBackground()); + Invalidate(); + } + else if( nType == StateChangedType::Mirroring ) + { + if (mpEdit) + mpEdit->CompatStateChanged(StateChangedType::Mirroring); + Resize(); + } +} + +void SpinField::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Edit::DataChanged(rDCEvt); + + if ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) + { + Resize(); + Invalidate(); + } +} + +tools::Rectangle* SpinField::ImplFindPartRect(const Point& rPt) +{ + if (maUpperRect.Contains(rPt)) + return &maUpperRect; + else if (maLowerRect.Contains(rPt)) + return &maLowerRect; + else + return nullptr; +} + +bool SpinField::PreNotify(NotifyEvent& rNEvt) +{ + if (rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if (pMouseEvt && !pMouseEvt->GetButtons() && !pMouseEvt->IsSynthetic() && !pMouseEvt->IsModifierChanged()) + { + // trigger redraw if mouse over state has changed + if( IsNativeControlSupported(ControlType::Spinbox, ControlPart::Entire) || + IsNativeControlSupported(ControlType::Spinbox, ControlPart::AllButtons) ) + { + tools::Rectangle* pRect = ImplFindPartRect( GetPointerPosPixel() ); + tools::Rectangle* pLastRect = ImplFindPartRect( GetLastPointerPosPixel() ); + if( pRect != pLastRect || (pMouseEvt->IsLeaveWindow() || pMouseEvt->IsEnterWindow()) ) + { + // FIXME: this is currently only on macOS + // check for other platforms that need similar handling + if (ImplGetSVData()->maNWFData.mbNoFocusRects && IsNativeWidgetEnabled() && + IsNativeControlSupported(ControlType::Editbox, ControlPart::Entire)) + { + ImplInvalidateOutermostBorder(this); + } + else + { + // paint directly + vcl::Region aRgn( GetOutDev()->GetActiveClipRegion() ); + if (pLastRect) + { + GetOutDev()->SetClipRegion(vcl::Region(*pLastRect)); + Invalidate(*pLastRect); + GetOutDev()->SetClipRegion( aRgn ); + } + if (pRect) + { + GetOutDev()->SetClipRegion(vcl::Region(*pRect)); + Invalidate(*pRect); + GetOutDev()->SetClipRegion( aRgn ); + } + } + } + } + } + } + + return Edit::PreNotify(rNEvt); +} + +void SpinField::EndDropDown() +{ + mbInDropDown = false; + Invalidate(tools::Rectangle(Point(), GetOutputSizePixel())); +} + +bool SpinField::ShowDropDown( bool ) +{ + return false; +} + +Size SpinField::CalcMinimumSizeForText(const OUString &rString) const +{ + Size aSz = Edit::CalcMinimumSizeForText(rString); + + if ( GetStyle() & WB_DROPDOWN ) + aSz.AdjustWidth(GetSettings().GetStyleSettings().GetScrollBarSize() ); + if ( GetStyle() & WB_SPIN ) + { + ImplControlValue aControlValue; + tools::Rectangle aArea( Point(), Size(100, aSz.Height())); + tools::Rectangle aEntireBound, aEntireContent, aEditBound, aEditContent; + if ( + GetNativeControlRegion(ControlType::Spinbox, ControlPart::Entire, + aArea, ControlState::NONE, aControlValue, aEntireBound, aEntireContent) && + GetNativeControlRegion(ControlType::Spinbox, ControlPart::SubEdit, + aArea, ControlState::NONE, aControlValue, aEditBound, aEditContent) + ) + { + aSz.AdjustWidth(aEntireContent.GetWidth() - aEditContent.GetWidth()); + } + else + { + aSz.AdjustWidth(maUpperRect.GetWidth() ); + } + } + + return aSz; +} + +Size SpinField::CalcMinimumSize() const +{ + return CalcMinimumSizeForText(GetText()); +} + +Size SpinField::GetOptimalSize() const +{ + return CalcMinimumSize(); +} + +Size SpinField::CalcSize(sal_Int32 nChars) const +{ + Size aSz = Edit::CalcSize( nChars ); + + if ( GetStyle() & WB_DROPDOWN ) + aSz.AdjustWidth(GetSettings().GetStyleSettings().GetScrollBarSize() ); + if ( GetStyle() & WB_SPIN ) + aSz.AdjustWidth(GetSettings().GetStyleSettings().GetSpinSize() ); + + return aSz; +} + +IMPL_LINK( SpinField, ImplTimeout, Timer*, pTimer, void ) +{ + if ( pTimer->GetTimeout() == static_cast<sal_uInt64>(MouseSettings::GetButtonStartRepeat()) ) + { + pTimer->SetTimeout( GetSettings().GetMouseSettings().GetButtonRepeat() ); + pTimer->Start(); + } + else + { + if ( mbInitialUp ) + Up(); + else + Down(); + } +} + +void SpinField::Draw(OutputDevice* pDev, const Point& rPos, SystemTextColorFlags nFlags) +{ + Edit::Draw(pDev, rPos, nFlags); + + WinBits nFieldStyle = GetStyle(); + if ( (nFlags & SystemTextColorFlags::NoControls ) || !( nFieldStyle & (WB_SPIN|WB_DROPDOWN) ) ) + return; + + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + AllSettings aOldSettings = pDev->GetSettings(); + + pDev->Push(); + pDev->SetMapMode(); + + tools::Rectangle aDD, aUp, aDown; + ImplCalcButtonAreas(pDev, aSize, aDD, aUp, aDown); + aDD.Move(aPos.X(), aPos.Y()); + aUp.Move(aPos.X(), aPos.Y()); + aUp.AdjustTop( 1 ); + aDown.Move(aPos.X(), aPos.Y()); + + Color aButtonTextColor; + if (nFlags & SystemTextColorFlags::Mono) + aButtonTextColor = COL_BLACK; + else + aButtonTextColor = GetSettings().GetStyleSettings().GetButtonTextColor(); + + if (GetStyle() & WB_DROPDOWN) + { + DecorationView aView( pDev ); + tools::Rectangle aInnerRect = aView.DrawButton( aDD, DrawButtonFlags::NoLightBorder ); + DrawSymbolFlags nSymbolStyle = IsEnabled() ? DrawSymbolFlags::NONE : DrawSymbolFlags::Disable; + aView.DrawSymbol(aInnerRect, SymbolType::SPIN_DOWN, aButtonTextColor, nSymbolStyle); + } + + if (GetStyle() & WB_SPIN) + { + ImplDrawSpinButton(*pDev, this, aUp, aDown, false, false); + } + + pDev->Pop(); + pDev->SetSettings(aOldSettings); + +} + +FactoryFunction SpinField::GetUITestFactory() const +{ + return SpinFieldUIObject::create; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/tabctrl.cxx b/vcl/source/control/tabctrl.cxx new file mode 100644 index 000000000..278fd59cd --- /dev/null +++ b/vcl/source/control/tabctrl.cxx @@ -0,0 +1,2459 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> +#include <sal/log.hxx> + +#include <vcl/notebookbar/notebookbar.hxx> +#include <vcl/svapp.hxx> +#include <vcl/help.hxx> +#include <vcl/event.hxx> +#include <vcl/menu.hxx> +#include <vcl/toolkit/button.hxx> +#include <vcl/tabpage.hxx> +#include <vcl/tabctrl.hxx> +#include <vcl/toolbox.hxx> +#include <vcl/layout.hxx> +#include <vcl/toolkit/lstbox.hxx> +#include <vcl/settings.hxx> +#include <vcl/uitest/uiobject.hxx> +#include <bitmaps.hlst> +#include <tools/json_writer.hxx> + +#include <svdata.hxx> +#include <window.h> + +#include <deque> +#include <unordered_map> +#include <vector> + +class ImplTabItem final +{ + sal_uInt16 m_nId; + +public: + VclPtr<TabPage> mpTabPage; + OUString maText; + OUString maFormatText; + OUString maHelpText; + OUString maAccessibleName; + OUString maAccessibleDescription; + OString maTabName; + tools::Rectangle maRect; + sal_uInt16 mnLine; + bool mbFullVisible; + bool m_bEnabled; ///< the tab / page is selectable + bool m_bVisible; ///< the tab / page can be visible + Image maTabImage; + + ImplTabItem(sal_uInt16 nId); + + sal_uInt16 id() const { return m_nId; } +}; + +ImplTabItem::ImplTabItem(sal_uInt16 nId) + : m_nId(nId) + , mnLine(0) + , mbFullVisible(false) + , m_bEnabled(true) + , m_bVisible(true) +{ +} + +struct ImplTabCtrlData +{ + std::unordered_map< int, int > maLayoutPageIdToLine; + std::unordered_map< int, int > maLayoutLineToPageId; + std::vector< ImplTabItem > maItemList; + VclPtr<ListBox> mpListBox; +}; + +// for the Tab positions +#define TAB_PAGERECT 0xFFFF + +void TabControl::ImplInit( vcl::Window* pParent, WinBits nStyle ) +{ + mbLayoutDirty = true; + + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + if ( !(nStyle & WB_NODIALOGCONTROL) ) + nStyle |= WB_DIALOGCONTROL; + + Control::ImplInit( pParent, nStyle, nullptr ); + + mnLastWidth = 0; + mnLastHeight = 0; + mnActPageId = 0; + mnCurPageId = 0; + mbFormat = true; + mbShowTabs = true; + mbRestoreHelpId = false; + mbSmallInvalidate = false; + mpTabCtrlData.reset(new ImplTabCtrlData); + mpTabCtrlData->mpListBox = nullptr; + + ImplInitSettings( true ); + + if( nStyle & WB_DROPDOWN ) + { + mpTabCtrlData->mpListBox = VclPtr<ListBox>::Create( this, WB_DROPDOWN ); + mpTabCtrlData->mpListBox->SetPosSizePixel( Point( 0, 0 ), Size( 200, 20 ) ); + mpTabCtrlData->mpListBox->SetSelectHdl( LINK( this, TabControl, ImplListBoxSelectHdl ) ); + mpTabCtrlData->mpListBox->Show(); + } + + // if the tabcontrol is drawn (ie filled) by a native widget, make sure all controls will have transparent background + // otherwise they will paint with a wrong background + if( IsNativeControlSupported(ControlType::TabPane, ControlPart::Entire) ) + EnableChildTransparentMode(); + + if (pParent && pParent->IsDialog()) + pParent->AddChildEventListener( LINK( this, TabControl, ImplWindowEventListener ) ); +} + +const vcl::Font& TabControl::GetCanonicalFont( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetTabFont(); +} + +const Color& TabControl::GetCanonicalTextColor( const StyleSettings& _rStyle ) const +{ + return _rStyle.GetTabTextColor(); +} + +void TabControl::ImplInitSettings( bool bBackground ) +{ + Control::ImplInitSettings(); + + if ( !bBackground ) + return; + + vcl::Window* pParent = GetParent(); + if ( !IsControlBackground() && + (pParent->IsChildTransparentModeEnabled() + || IsNativeControlSupported(ControlType::TabPane, ControlPart::Entire) + || IsNativeControlSupported(ControlType::TabItem, ControlPart::Entire) ) ) + + { + // set transparent mode for NWF tabcontrols to have + // the background always cleared properly + EnableChildTransparentMode(); + SetParentClipMode( ParentClipMode::NoClip ); + SetPaintTransparent( true ); + SetBackground(); + ImplGetWindowImpl()->mbUseNativeFocus = ImplGetSVData()->maNWFData.mbNoFocusRects; + } + else + { + EnableChildTransparentMode( false ); + SetParentClipMode(); + SetPaintTransparent( false ); + + if ( IsControlBackground() ) + SetBackground( GetControlBackground() ); + else + SetBackground( pParent->GetBackground() ); + } +} + +void TabControl::ImplFreeLayoutData() +{ + if( HasLayoutData() ) + { + ImplClearLayoutData(); + mpTabCtrlData->maLayoutPageIdToLine.clear(); + mpTabCtrlData->maLayoutLineToPageId.clear(); + } +} + +TabControl::TabControl( vcl::Window* pParent, WinBits nStyle ) : + Control( WindowType::TABCONTROL ) +{ + ImplInit( pParent, nStyle ); + SAL_INFO( "vcl", "*** TABCONTROL no notabs? " << (( GetStyle() & WB_NOBORDER ) ? "true" : "false") ); +} + +TabControl::~TabControl() +{ + disposeOnce(); +} + +void TabControl::dispose() +{ + Window *pParent = GetParent(); + if (pParent && pParent->IsDialog()) + GetParent()->RemoveChildEventListener( LINK( this, TabControl, ImplWindowEventListener ) ); + + ImplFreeLayoutData(); + + // delete TabCtrl data + if (mpTabCtrlData) + mpTabCtrlData->mpListBox.disposeAndClear(); + mpTabCtrlData.reset(); + Control::dispose(); +} + +ImplTabItem* TabControl::ImplGetItem( sal_uInt16 nId ) const +{ + for (auto & item : mpTabCtrlData->maItemList) + { + if (item.id() == nId) + return &item; + } + + return nullptr; +} + +Size TabControl::ImplGetItemSize( ImplTabItem* pItem, tools::Long nMaxWidth ) +{ + pItem->maFormatText = pItem->maText; + Size aSize( GetOutDev()->GetCtrlTextWidth( pItem->maFormatText ), GetTextHeight() ); + Size aImageSize( 0, 0 ); + if( !!pItem->maTabImage ) + { + aImageSize = pItem->maTabImage.GetSizePixel(); + if( !pItem->maFormatText.isEmpty() ) + aImageSize.AdjustWidth(GetTextHeight()/4 ); + } + aSize.AdjustWidth(aImageSize.Width() ); + if( aImageSize.Height() > aSize.Height() ) + aSize.setHeight( aImageSize.Height() ); + + aSize.AdjustWidth(TAB_TABOFFSET_X*2 ); + aSize.AdjustHeight(TAB_TABOFFSET_Y*2 ); + + tools::Rectangle aCtrlRegion( Point( 0, 0 ), aSize ); + tools::Rectangle aBoundingRgn, aContentRgn; + const TabitemValue aControlValue(tools::Rectangle(TAB_TABOFFSET_X, TAB_TABOFFSET_Y, + aSize.Width() - TAB_TABOFFSET_X * 2, + aSize.Height() - TAB_TABOFFSET_Y * 2)); + if(GetNativeControlRegion( ControlType::TabItem, ControlPart::Entire, aCtrlRegion, + ControlState::ENABLED, aControlValue, + aBoundingRgn, aContentRgn ) ) + { + return aContentRgn.GetSize(); + } + + // For languages with short names (e.g. Chinese), because the space is + // normally only one pixel per char + if ( pItem->maFormatText.getLength() < TAB_EXTRASPACE_X ) + aSize.AdjustWidth(TAB_EXTRASPACE_X-pItem->maFormatText.getLength() ); + + // shorten Text if needed + if ( aSize.Width()+4 >= nMaxWidth ) + { + OUString aAppendStr("..."); + pItem->maFormatText += aAppendStr; + do + { + if (pItem->maFormatText.getLength() > aAppendStr.getLength()) + pItem->maFormatText = pItem->maFormatText.replaceAt( pItem->maFormatText.getLength()-aAppendStr.getLength()-1, 1, u"" ); + aSize.setWidth( GetOutDev()->GetCtrlTextWidth( pItem->maFormatText ) ); + aSize.AdjustWidth(aImageSize.Width() ); + aSize.AdjustWidth(TAB_TABOFFSET_X*2 ); + } + while ( (aSize.Width()+4 >= nMaxWidth) && (pItem->maFormatText.getLength() > aAppendStr.getLength()) ); + if ( aSize.Width()+4 >= nMaxWidth ) + { + pItem->maFormatText = "."; + aSize.setWidth( 1 ); + } + } + + if( pItem->maFormatText.isEmpty() ) + { + if( aSize.Height() < aImageSize.Height()+4 ) //leave space for focus rect + aSize.setHeight( aImageSize.Height()+4 ); + } + + return aSize; +} + +// Feel free to move this to some more general place for reuse +// http://en.wikipedia.org/wiki/Word_wrap#Minimum_raggedness +// Mostly based on Alexey Frunze's nifty example at +// http://stackoverflow.com/questions/9071205/balanced-word-wrap-minimum-raggedness-in-php +namespace MinimumRaggednessWrap +{ + static std::deque<size_t> GetEndOfLineIndexes(const std::vector<sal_Int32>& rWidthsOf, sal_Int32 nLineWidth) + { + ++nLineWidth; + + size_t nWidthsCount = rWidthsOf.size(); + std::vector<sal_Int32> aCosts(nWidthsCount * nWidthsCount); + + // cost function c(i, j) that computes the cost of a line consisting of + // the words Word[i] to Word[j] + for (size_t i = 0; i < nWidthsCount; ++i) + { + for (size_t j = 0; j < nWidthsCount; ++j) + { + if (j >= i) + { + sal_Int32 c = nLineWidth - (j - i); + for (size_t k = i; k <= j; ++k) + c -= rWidthsOf[k]; + c = (c >= 0) ? c * c : SAL_MAX_INT32; + aCosts[j * nWidthsCount + i] = c; + } + else + { + aCosts[j * nWidthsCount + i] = SAL_MAX_INT32; + } + } + } + + std::vector<sal_Int32> aFunction(nWidthsCount); + std::vector<sal_Int32> aWrapPoints(nWidthsCount); + + // f(j) in aFunction[], collect wrap points in aWrapPoints[] + for (size_t j = 0; j < nWidthsCount; ++j) + { + aFunction[j] = aCosts[j * nWidthsCount]; + if (aFunction[j] == SAL_MAX_INT32) + { + for (size_t k = 0; k < j; ++k) + { + sal_Int32 s; + if (aFunction[k] == SAL_MAX_INT32 || aCosts[j * nWidthsCount + k + 1] == SAL_MAX_INT32) + s = SAL_MAX_INT32; + else + s = aFunction[k] + aCosts[j * nWidthsCount + k + 1]; + if (aFunction[j] > s) + { + aFunction[j] = s; + aWrapPoints[j] = k + 1; + } + } + } + } + + std::deque<size_t> aSolution; + + // no solution + if (aFunction[nWidthsCount - 1] == SAL_MAX_INT32) + return aSolution; + + // optimal solution + size_t j = nWidthsCount - 1; + while (true) + { + aSolution.push_front(j); + if (!aWrapPoints[j]) + break; + j = aWrapPoints[j] - 1; + } + + return aSolution; + } +}; + +static void lcl_AdjustSingleLineTabs(tools::Long nMaxWidth, ImplTabCtrlData *pTabCtrlData) +{ + if (!ImplGetSVData()->maNWFData.mbCenteredTabs) + return; + + int nRightSpace = nMaxWidth; // space left on the right by the tabs + for (auto const& item : pTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + nRightSpace -= item.maRect.Right() - item.maRect.Left(); + } + nRightSpace /= 2; + + for (auto& item : pTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + item.maRect.AdjustLeft(nRightSpace); + item.maRect.AdjustRight(nRightSpace); + } +} + +bool TabControl::ImplPlaceTabs( tools::Long nWidth ) +{ + if ( nWidth <= 0 ) + return false; + if ( mpTabCtrlData->maItemList.empty() ) + return false; + + tools::Long nMaxWidth = nWidth; + + const tools::Long nOffsetX = 2; + const tools::Long nOffsetY = 2; + + //fdo#66435 throw Knuth/Tex minimum raggedness algorithm at the problem + //of ugly bare tabs on lines of their own + + //collect widths + std::vector<sal_Int32> aWidths; + for (auto & item : mpTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + aWidths.push_back(ImplGetItemSize(&item, nMaxWidth).Width()); + } + + //aBreakIndexes will contain the indexes of the last tab on each row + std::deque<size_t> aBreakIndexes(MinimumRaggednessWrap::GetEndOfLineIndexes(aWidths, nMaxWidth - nOffsetX - 2)); + + tools::Long nX = nOffsetX; + tools::Long nY = nOffsetY; + + sal_uInt16 nLines = 0; + sal_uInt16 nCurLine = 0; + + tools::Long nLineWidthAry[100]; + sal_uInt16 nLinePosAry[101]; + nLineWidthAry[0] = 0; + nLinePosAry[0] = 0; + + size_t nIndex = 0; + + for (auto & item : mpTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + + Size aSize = ImplGetItemSize( &item, nMaxWidth ); + + bool bNewLine = false; + if (!aBreakIndexes.empty() && nIndex > aBreakIndexes.front()) + { + aBreakIndexes.pop_front(); + bNewLine = true; + } + + if ( bNewLine && (nWidth > 2+nOffsetX) ) + { + if ( nLines == 99 ) + break; + + nX = nOffsetX; + nY += aSize.Height(); + nLines++; + nLineWidthAry[nLines] = 0; + nLinePosAry[nLines] = nIndex; + } + + tools::Rectangle aNewRect( Point( nX, nY ), aSize ); + if ( mbSmallInvalidate && (item.maRect != aNewRect) ) + mbSmallInvalidate = false; + item.maRect = aNewRect; + item.mnLine = nLines; + item.mbFullVisible = true; + + nLineWidthAry[nLines] += aSize.Width(); + nX += aSize.Width(); + + if (item.id() == mnCurPageId) + nCurLine = nLines; + + ++nIndex; + } + + if (nLines) // two or more lines + { + tools::Long nLineHeightAry[100]; + tools::Long nIH = 0; + for (const auto& item : mpTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + nIH = item.maRect.Bottom() - 1; + break; + } + + for ( sal_uInt16 i = 0; i < nLines+1; i++ ) + { + if ( i <= nCurLine ) + nLineHeightAry[i] = nIH*(nLines-(nCurLine-i)); + else + nLineHeightAry[i] = nIH*(i-nCurLine-1); + } + + nLinePosAry[nLines+1] = static_cast<sal_uInt16>(mpTabCtrlData->maItemList.size()); + + tools::Long nDX = 0; + tools::Long nModDX = 0; + tools::Long nIDX = 0; + + sal_uInt16 i = 0; + sal_uInt16 n = 0; + + for (auto & item : mpTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + + if ( i == nLinePosAry[n] ) + { + if ( n == nLines+1 ) + break; + + nIDX = 0; + if( nLinePosAry[n+1]-i > 0 ) + { + nDX = ( nWidth - nOffsetX - nLineWidthAry[n] ) / ( nLinePosAry[n+1] - i ); + nModDX = ( nWidth - nOffsetX - nLineWidthAry[n] ) % ( nLinePosAry[n+1] - i ); + } + else + { + // FIXME: this is a case of tabctrl way too small + nDX = 0; + nModDX = 0; + } + n++; + } + + item.maRect.AdjustLeft(nIDX ); + item.maRect.AdjustRight(nIDX + nDX ); + item.maRect.SetTop( nLineHeightAry[n-1] ); + item.maRect.SetBottom(nLineHeightAry[n-1] + nIH - 1); + nIDX += nDX; + + if ( nModDX ) + { + nIDX++; + item.maRect.AdjustRight( 1 ); + nModDX--; + } + + i++; + } + } + else // only one line + lcl_AdjustSingleLineTabs(nMaxWidth, mpTabCtrlData.get()); + + return true; +} + +tools::Rectangle TabControl::ImplGetTabRect( sal_uInt16 nItemPos, tools::Long nWidth, tools::Long nHeight ) +{ + Size aWinSize = Control::GetOutputSizePixel(); + if ( nWidth < 0 ) + nWidth = aWinSize.Width(); + if ( nHeight < 0 ) + nHeight = aWinSize.Height(); + + if ( mpTabCtrlData->maItemList.empty() ) + { + tools::Long nW = nWidth-TAB_OFFSET*2; + tools::Long nH = nHeight-TAB_OFFSET*2; + return (nW > 0 && nH > 0) + ? tools::Rectangle(Point(TAB_OFFSET, TAB_OFFSET), Size(nW, nH)) + : tools::Rectangle(); + } + + if ( nItemPos == TAB_PAGERECT ) + { + sal_uInt16 nLastPos; + if ( mnCurPageId ) + nLastPos = GetPagePos( mnCurPageId ); + else + nLastPos = 0; + + tools::Rectangle aRect = ImplGetTabRect( nLastPos, nWidth, nHeight ); + if (aRect.IsEmpty()) + return aRect; + + // with show-tabs of true (the usual) the page rect is from under the + // visible tab to the bottom of the TabControl, otherwise it extends + // from the top of the TabControl + tools::Long nTabBottom = mbShowTabs ? aRect.Bottom() : 0; + + tools::Long nW = nWidth-TAB_OFFSET*2; + tools::Long nH = nHeight - nTabBottom - TAB_OFFSET*2; + return (nW > 0 && nH > 0) + ? tools::Rectangle( Point( TAB_OFFSET, nTabBottom + TAB_OFFSET ), Size( nW, nH ) ) + : tools::Rectangle(); + } + + ImplTabItem* const pItem = (nItemPos < mpTabCtrlData->maItemList.size()) + ? &mpTabCtrlData->maItemList[nItemPos] : nullptr; + return ImplGetTabRect(pItem, nWidth, nHeight); +} + +tools::Rectangle TabControl::ImplGetTabRect(const ImplTabItem* pItem, tools::Long nWidth, tools::Long nHeight) +{ + if ((nWidth <= 1) || (nHeight <= 0) || !pItem || !pItem->m_bVisible) + return tools::Rectangle(); + + nWidth -= 1; + + if ( mbFormat || (mnLastWidth != nWidth) || (mnLastHeight != nHeight) ) + { + vcl::Font aFont( GetFont() ); + aFont.SetTransparent( true ); + SetFont( aFont ); + + bool bRet = ImplPlaceTabs( nWidth ); + if ( !bRet ) + return tools::Rectangle(); + + mnLastWidth = nWidth; + mnLastHeight = nHeight; + mbFormat = false; + } + + return pItem->maRect; +} + +void TabControl::ImplChangeTabPage( sal_uInt16 nId, sal_uInt16 nOldId ) +{ + ImplFreeLayoutData(); + + ImplTabItem* pOldItem = ImplGetItem( nOldId ); + ImplTabItem* pItem = ImplGetItem( nId ); + TabPage* pOldPage = pOldItem ? pOldItem->mpTabPage.get() : nullptr; + TabPage* pPage = pItem ? pItem->mpTabPage.get() : nullptr; + vcl::Window* pCtrlParent = GetParent(); + + if ( IsReallyVisible() && IsUpdateMode() ) + { + sal_uInt16 nPos = GetPagePos( nId ); + tools::Rectangle aRect = ImplGetTabRect( nPos ); + + if ( !pOldItem || !pItem || (pOldItem->mnLine != pItem->mnLine) ) + { + aRect.SetLeft( 0 ); + aRect.SetTop( 0 ); + aRect.SetRight( Control::GetOutputSizePixel().Width() ); + } + else + { + aRect.AdjustLeft( -3 ); + aRect.AdjustTop( -2 ); + aRect.AdjustRight(3 ); + Invalidate( aRect ); + nPos = GetPagePos( nOldId ); + aRect = ImplGetTabRect( nPos ); + aRect.AdjustLeft( -3 ); + aRect.AdjustTop( -2 ); + aRect.AdjustRight(3 ); + } + Invalidate( aRect ); + } + + if ( pOldPage == pPage ) + return; + + tools::Rectangle aRect = ImplGetTabRect( TAB_PAGERECT ); + + if ( pOldPage ) + { + if ( mbRestoreHelpId ) + pCtrlParent->SetHelpId( OString() ); + } + + if ( pPage ) + { + if ( GetStyle() & WB_NOBORDER ) + { + tools::Rectangle aRectNoTab(Point(0, 0), GetSizePixel()); + pPage->SetPosSizePixel( aRectNoTab.TopLeft(), aRectNoTab.GetSize() ); + } + else + pPage->SetPosSizePixel( aRect.TopLeft(), aRect.GetSize() ); + + // activate page here so the controls can be switched + // also set the help id of the parent window to that of the tab page + if ( GetHelpId().isEmpty() ) + { + mbRestoreHelpId = true; + pCtrlParent->SetHelpId( pPage->GetHelpId() ); + } + + pPage->Show(); + + if ( pOldPage && pOldPage->HasChildPathFocus() ) + { + vcl::Window* pFirstChild = pPage->ImplGetDlgWindow( 0, GetDlgWindowType::First ); + if ( pFirstChild ) + pFirstChild->ImplControlFocus( GetFocusFlags::Init ); + else + GrabFocus(); + } + } + + if ( pOldPage ) + pOldPage->Hide(); + + // Invalidate the same region that will be send to NWF + // to always allow for bitmap caching + // see Window::DrawNativeControl() + if( IsNativeControlSupported( ControlType::TabPane, ControlPart::Entire ) ) + { + aRect.AdjustLeft( -(TAB_OFFSET) ); + aRect.AdjustTop( -(TAB_OFFSET) ); + aRect.AdjustRight(TAB_OFFSET ); + aRect.AdjustBottom(TAB_OFFSET ); + } + + Invalidate( aRect ); +} + +bool TabControl::ImplPosCurTabPage() +{ + // resize/position current TabPage + ImplTabItem* pItem = ImplGetItem( GetCurPageId() ); + if ( pItem && pItem->mpTabPage ) + { + if ( GetStyle() & WB_NOBORDER ) + { + tools::Rectangle aRectNoTab(Point(0, 0), GetSizePixel()); + pItem->mpTabPage->SetPosSizePixel( aRectNoTab.TopLeft(), aRectNoTab.GetSize() ); + return true; + } + tools::Rectangle aRect = ImplGetTabRect( TAB_PAGERECT ); + pItem->mpTabPage->SetPosSizePixel( aRect.TopLeft(), aRect.GetSize() ); + return true; + } + + return false; +} + +void TabControl::ImplActivateTabPage( bool bNext ) +{ + sal_uInt16 nCurPos = GetPagePos( GetCurPageId() ); + + if ( bNext ) + nCurPos = (nCurPos + 1) % GetPageCount(); + else + { + if ( !nCurPos ) + nCurPos = GetPageCount()-1; + else + nCurPos--; + } + + SelectTabPage( GetPageId( nCurPos ) ); +} + +void TabControl::ImplShowFocus() +{ + if ( !GetPageCount() || mpTabCtrlData->mpListBox ) + return; + + sal_uInt16 nCurPos = GetPagePos( mnCurPageId ); + tools::Rectangle aRect = ImplGetTabRect( nCurPos ); + const ImplTabItem& rItem = mpTabCtrlData->maItemList[ nCurPos ]; + Size aTabSize = aRect.GetSize(); + Size aImageSize( 0, 0 ); + tools::Long nTextHeight = GetTextHeight(); + tools::Long nTextWidth = GetOutDev()->GetCtrlTextWidth( rItem.maFormatText ); + sal_uInt16 nOff; + + if ( !(GetSettings().GetStyleSettings().GetOptions() & StyleSettingsOptions::Mono) ) + nOff = 1; + else + nOff = 0; + + if( !! rItem.maTabImage ) + { + aImageSize = rItem.maTabImage.GetSizePixel(); + if( !rItem.maFormatText.isEmpty() ) + aImageSize.AdjustWidth(GetTextHeight()/4 ); + } + + if( !rItem.maFormatText.isEmpty() ) + { + // show focus around text + aRect.SetLeft( aRect.Left()+aImageSize.Width()+((aTabSize.Width()-nTextWidth-aImageSize.Width())/2)-nOff-1-1 ); + aRect.SetTop( aRect.Top()+((aTabSize.Height()-nTextHeight)/2)-1-1 ); + aRect.SetRight( aRect.Left()+nTextWidth+2 ); + aRect.SetBottom( aRect.Top()+nTextHeight+2 ); + } + else + { + // show focus around image + tools::Long nXPos = aRect.Left()+((aTabSize.Width()-nTextWidth-aImageSize.Width())/2)-nOff-1; + tools::Long nYPos = aRect.Top(); + if( aImageSize.Height() < aRect.GetHeight() ) + nYPos += (aRect.GetHeight() - aImageSize.Height())/2; + + aRect.SetLeft( nXPos - 2 ); + aRect.SetTop( nYPos - 2 ); + aRect.SetRight( aRect.Left() + aImageSize.Width() + 4 ); + aRect.SetBottom( aRect.Top() + aImageSize.Height() + 4 ); + } + ShowFocus( aRect ); +} + +void TabControl::ImplDrawItem(vcl::RenderContext& rRenderContext, ImplTabItem const * pItem, const tools::Rectangle& rCurRect, + bool bFirstInGroup, bool bLastInGroup ) +{ + if (!pItem->m_bVisible || pItem->maRect.IsEmpty()) + return; + + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + tools::Rectangle aRect = pItem->maRect; + tools::Long nLeftBottom = aRect.Bottom(); + tools::Long nRightBottom = aRect.Bottom(); + bool bLeftBorder = true; + bool bRightBorder = true; + sal_uInt16 nOff; + bool bNativeOK = false; + + sal_uInt16 nOff2 = 0; + sal_uInt16 nOff3 = 0; + + if (!(rStyleSettings.GetOptions() & StyleSettingsOptions::Mono)) + nOff = 1; + else + nOff = 0; + + // if this is the active Page, we have to draw a little more + if (pItem->id() == mnCurPageId) + { + nOff2 = 2; + if (!ImplGetSVData()->maNWFData.mbNoActiveTabTextRaise) + nOff3 = 1; + } + else + { + Point aLeftTestPos = aRect.BottomLeft(); + Point aRightTestPos = aRect.BottomRight(); + if (aLeftTestPos.Y() == rCurRect.Bottom()) + { + aLeftTestPos.AdjustX( -2 ); + if (rCurRect.Contains(aLeftTestPos)) + bLeftBorder = false; + aRightTestPos.AdjustX(2 ); + if (rCurRect.Contains(aRightTestPos)) + bRightBorder = false; + } + else + { + if (rCurRect.Contains(aLeftTestPos)) + nLeftBottom -= 2; + if (rCurRect.Contains(aRightTestPos)) + nRightBottom -= 2; + } + } + + ControlState nState = ControlState::NONE; + + if (pItem->id() == mnCurPageId) + { + nState |= ControlState::SELECTED; + // only the selected item can be focused + if (HasFocus()) + nState |= ControlState::FOCUSED; + } + if (IsEnabled()) + nState |= ControlState::ENABLED; + if (IsMouseOver() && pItem->maRect.Contains(GetPointerPosPixel())) + { + nState |= ControlState::ROLLOVER; + for (auto const& item : mpTabCtrlData->maItemList) + if ((&item != pItem) && item.m_bVisible && item.maRect.Contains(GetPointerPosPixel())) + { + nState &= ~ControlState::ROLLOVER; // avoid multiple highlighted tabs + break; + } + assert(nState & ControlState::ROLLOVER); + } + + bNativeOK = rRenderContext.IsNativeControlSupported(ControlType::TabItem, ControlPart::Entire); + if ( bNativeOK ) + { + TabitemValue tiValue(tools::Rectangle(pItem->maRect.Left() + TAB_TABOFFSET_X, + pItem->maRect.Top() + TAB_TABOFFSET_Y, + pItem->maRect.Right() - TAB_TABOFFSET_X, + pItem->maRect.Bottom() - TAB_TABOFFSET_Y)); + if (pItem->maRect.Left() < 5) + tiValue.mnAlignment |= TabitemFlags::LeftAligned; + if (pItem->maRect.Right() > mnLastWidth - 5) + tiValue.mnAlignment |= TabitemFlags::RightAligned; + if (bFirstInGroup) + tiValue.mnAlignment |= TabitemFlags::FirstInGroup; + if (bLastInGroup) + tiValue.mnAlignment |= TabitemFlags::LastInGroup; + + tools::Rectangle aCtrlRegion( pItem->maRect ); + aCtrlRegion.AdjustBottom(TabPaneValue::m_nOverlap); + bNativeOK = rRenderContext.DrawNativeControl(ControlType::TabItem, ControlPart::Entire, + aCtrlRegion, nState, tiValue, OUString() ); + } + + if (!bNativeOK) + { + if (!(rStyleSettings.GetOptions() & StyleSettingsOptions::Mono)) + { + rRenderContext.SetLineColor(rStyleSettings.GetLightColor()); + rRenderContext.DrawPixel(Point(aRect.Left() + 1 - nOff2, aRect.Top() + 1 - nOff2)); // diagonally indented top-left pixel + if (bLeftBorder) + { + rRenderContext.DrawLine(Point(aRect.Left() - nOff2, aRect.Top() + 2 - nOff2), + Point(aRect.Left() - nOff2, nLeftBottom - 1)); + } + rRenderContext.DrawLine(Point(aRect.Left() + 2 - nOff2, aRect.Top() - nOff2), // top line starting 2px from left border + Point(aRect.Right() + nOff2 - 3, aRect.Top() - nOff2)); // ending 3px from right border + + if (bRightBorder) + { + rRenderContext.SetLineColor(rStyleSettings.GetShadowColor()); + rRenderContext.DrawLine(Point(aRect.Right() + nOff2 - 2, aRect.Top() + 1 - nOff2), + Point(aRect.Right() + nOff2 - 2, nRightBottom - 1)); + + rRenderContext.SetLineColor(rStyleSettings.GetDarkShadowColor()); + rRenderContext.DrawLine(Point(aRect.Right() + nOff2 - 1, aRect.Top() + 3 - nOff2), + Point(aRect.Right() + nOff2 - 1, nRightBottom - 1)); + } + } + else + { + rRenderContext.SetLineColor(COL_BLACK); + rRenderContext.DrawPixel(Point(aRect.Left() + 1 - nOff2, aRect.Top() + 1 - nOff2)); + rRenderContext.DrawPixel(Point(aRect.Right() + nOff2 - 2, aRect.Top() + 1 - nOff2)); + if (bLeftBorder) + { + rRenderContext.DrawLine(Point(aRect.Left() - nOff2, aRect.Top() + 2 - nOff2), + Point(aRect.Left() - nOff2, nLeftBottom - 1)); + } + rRenderContext.DrawLine(Point(aRect.Left() + 2 - nOff2, aRect.Top() - nOff2), + Point(aRect.Right() - 3, aRect.Top() - nOff2)); + if (bRightBorder) + { + rRenderContext.DrawLine(Point(aRect.Right() + nOff2 - 1, aRect.Top() + 2 - nOff2), + Point(aRect.Right() + nOff2 - 1, nRightBottom - 1)); + } + } + } + + // set font accordingly, current item is painted bold + // we set the font attributes always before drawing to be re-entrant (DrawNativeControl may trigger additional paints) + vcl::Font aFont(rRenderContext.GetFont()); + aFont.SetTransparent(true); + rRenderContext.SetFont(aFont); + + Size aTabSize = aRect.GetSize(); + Size aImageSize(0, 0); + tools::Long nTextHeight = rRenderContext.GetTextHeight(); + tools::Long nTextWidth = rRenderContext.GetCtrlTextWidth(pItem->maFormatText); + if (!!pItem->maTabImage) + { + aImageSize = pItem->maTabImage.GetSizePixel(); + if (!pItem->maFormatText.isEmpty()) + aImageSize.AdjustWidth(GetTextHeight() / 4 ); + } + tools::Long nXPos = aRect.Left() + ((aTabSize.Width() - nTextWidth - aImageSize.Width()) / 2) - nOff - nOff3; + tools::Long nYPos = aRect.Top() + ((aTabSize.Height() - nTextHeight) / 2) - nOff3; + if (!pItem->maFormatText.isEmpty()) + { + DrawTextFlags nStyle = DrawTextFlags::Mnemonic; + if (!pItem->m_bEnabled) + nStyle |= DrawTextFlags::Disable; + + Color aColor(rStyleSettings.GetTabTextColor()); + if (nState & ControlState::SELECTED) + aColor = rStyleSettings.GetTabHighlightTextColor(); + else if (nState & ControlState::ROLLOVER) + aColor = rStyleSettings.GetTabRolloverTextColor(); + + Color aOldColor(rRenderContext.GetTextColor()); + rRenderContext.SetTextColor(aColor); + + const tools::Rectangle aOutRect(nXPos + aImageSize.Width(), nYPos, + nXPos + aImageSize.Width() + nTextWidth, nYPos + nTextHeight); + DrawControlText(rRenderContext, aOutRect, pItem->maFormatText, nStyle, + nullptr, nullptr); + + rRenderContext.SetTextColor(aOldColor); + } + + if (!!pItem->maTabImage) + { + Point aImgTL( nXPos, aRect.Top() ); + if (aImageSize.Height() < aRect.GetHeight()) + aImgTL.AdjustY((aRect.GetHeight() - aImageSize.Height()) / 2 ); + rRenderContext.DrawImage(aImgTL, pItem->maTabImage, pItem->m_bEnabled ? DrawImageFlags::NONE : DrawImageFlags::Disable ); + } +} + +bool TabControl::ImplHandleKeyEvent( const KeyEvent& rKeyEvent ) +{ + bool bRet = false; + + if ( GetPageCount() > 1 ) + { + vcl::KeyCode aKeyCode = rKeyEvent.GetKeyCode(); + sal_uInt16 nKeyCode = aKeyCode.GetCode(); + + if ( aKeyCode.IsMod1() ) + { + if ( aKeyCode.IsShift() || (nKeyCode == KEY_PAGEUP) ) + { + if ( (nKeyCode == KEY_TAB) || (nKeyCode == KEY_PAGEUP) ) + { + ImplActivateTabPage( false ); + bRet = true; + } + } + else + { + if ( (nKeyCode == KEY_TAB) || (nKeyCode == KEY_PAGEDOWN) ) + { + ImplActivateTabPage( true ); + bRet = true; + } + } + } + } + + return bRet; +} + +IMPL_LINK_NOARG(TabControl, ImplListBoxSelectHdl, ListBox&, void) +{ + SelectTabPage( GetPageId( mpTabCtrlData->mpListBox->GetSelectedEntryPos() ) ); +} + +IMPL_LINK( TabControl, ImplWindowEventListener, VclWindowEvent&, rEvent, void ) +{ + if ( rEvent.GetId() == VclEventId::WindowKeyInput ) + { + // Do not handle events from TabControl or its children, which is done in Notify(), where the events can be consumed. + if ( !IsWindowOrChild( rEvent.GetWindow() ) ) + { + KeyEvent* pKeyEvent = static_cast< KeyEvent* >(rEvent.GetData()); + ImplHandleKeyEvent( *pKeyEvent ); + } + } +} + +void TabControl::MouseButtonDown( const MouseEvent& rMEvt ) +{ + if (mpTabCtrlData->mpListBox || !rMEvt.IsLeft()) + return; + + ImplTabItem *pItem = ImplGetItem(rMEvt.GetPosPixel()); + if (pItem && pItem->m_bEnabled) + SelectTabPage(pItem->id()); +} + +void TabControl::KeyInput( const KeyEvent& rKEvt ) +{ + if( mpTabCtrlData->mpListBox ) + mpTabCtrlData->mpListBox->KeyInput( rKEvt ); + else if ( GetPageCount() > 1 ) + { + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + sal_uInt16 nKeyCode = aKeyCode.GetCode(); + + if ( (nKeyCode == KEY_LEFT) || (nKeyCode == KEY_RIGHT) ) + { + bool bNext = (nKeyCode == KEY_RIGHT); + ImplActivateTabPage( bNext ); + } + } + + Control::KeyInput( rKEvt ); +} + +static bool lcl_canPaint(const vcl::RenderContext& rRenderContext, const tools::Rectangle& rDrawRect, + const tools::Rectangle& rItemRect) +{ + vcl::Region aClipRgn(rRenderContext.GetActiveClipRegion()); + aClipRgn.Intersect(rItemRect); + if (!rDrawRect.IsEmpty()) + aClipRgn.Intersect(rDrawRect); + return !aClipRgn.IsEmpty(); +} + +void TabControl::Paint( vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + if (GetStyle() & WB_NOBORDER) + return; + + Control::Paint(rRenderContext, rRect); + + HideFocus(); + + // reformat if needed + tools::Rectangle aRect = ImplGetTabRect(TAB_PAGERECT); + + // find current item + ImplTabItem* pCurItem = nullptr; + for (auto & item : mpTabCtrlData->maItemList) + { + if (item.id() == mnCurPageId) + { + pCurItem = &item; + break; + } + } + + // Draw the TabPage border + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + tools::Rectangle aCurRect; + aRect.AdjustLeft( -(TAB_OFFSET) ); + aRect.AdjustTop( -(TAB_OFFSET) ); + aRect.AdjustRight(TAB_OFFSET ); + aRect.AdjustBottom(TAB_OFFSET ); + + // if we have an invisible tabpage or no tabpage at all the tabpage rect should be + // increased to avoid round corners that might be drawn by a theme + // in this case we're only interested in the top border of the tabpage because the tabitems are used + // standalone (eg impress) + bool bNoTabPage = false; + TabPage* pCurPage = pCurItem ? pCurItem->mpTabPage.get() : nullptr; + if (!pCurPage || !pCurPage->IsVisible()) + { + bNoTabPage = true; + aRect.AdjustLeft( -10 ); + aRect.AdjustRight(10 ); + } + + if (rRenderContext.IsNativeControlSupported(ControlType::TabPane, ControlPart::Entire)) + { + const bool bPaneWithHeader = mbShowTabs && rRenderContext.IsNativeControlSupported(ControlType::TabPane, ControlPart::TabPaneWithHeader); + tools::Rectangle aHeaderRect(aRect.Left(), 0, aRect.Right(), aRect.Top()); + if (bPaneWithHeader) + { + aRect.SetTop(0); + if (mpTabCtrlData->maItemList.size()) + { + tools::Long nRight = 0; + for (const auto &item : mpTabCtrlData->maItemList) + if (item.m_bVisible) + nRight = item.maRect.Right(); + assert(nRight); + aHeaderRect.SetRight(nRight); + } + } + const TabPaneValue aTabPaneValue(aHeaderRect, pCurItem ? pCurItem->maRect : tools::Rectangle()); + + ControlState nState = ControlState::ENABLED; + if (!IsEnabled()) + nState &= ~ControlState::ENABLED; + if (HasFocus()) + nState |= ControlState::FOCUSED; + + if (lcl_canPaint(rRenderContext, rRect, aRect)) + rRenderContext.DrawNativeControl(ControlType::TabPane, ControlPart::Entire, + aRect, nState, aTabPaneValue, OUString()); + + if (!bPaneWithHeader && rRenderContext.IsNativeControlSupported(ControlType::TabHeader, ControlPart::Entire) + && lcl_canPaint(rRenderContext, rRect, aHeaderRect)) + rRenderContext.DrawNativeControl(ControlType::TabHeader, ControlPart::Entire, + aHeaderRect, nState, aTabPaneValue, OUString()); + } + else + { + tools::Long nTopOff = 1; + if (!(rStyleSettings.GetOptions() & StyleSettingsOptions::Mono)) + rRenderContext.SetLineColor(rStyleSettings.GetLightColor()); + else + rRenderContext.SetLineColor(COL_BLACK); + if (mbShowTabs && pCurItem && !pCurItem->maRect.IsEmpty()) + { + aCurRect = pCurItem->maRect; + rRenderContext.DrawLine(aRect.TopLeft(), Point(aCurRect.Left() - 2, aRect.Top())); + if (aCurRect.Right() + 1 < aRect.Right()) + { + rRenderContext.DrawLine(Point(aCurRect.Right(), aRect.Top()), aRect.TopRight()); + } + else + { + nTopOff = 0; + } + } + else + rRenderContext.DrawLine(aRect.TopLeft(), aRect.TopRight()); + + rRenderContext.DrawLine(aRect.TopLeft(), aRect.BottomLeft()); + + if (!(rStyleSettings.GetOptions() & StyleSettingsOptions::Mono)) + { + // if we have not tab page the bottom line of the tab page + // directly touches the tab items, so choose a color that fits seamlessly + if (bNoTabPage) + rRenderContext.SetLineColor(rStyleSettings.GetDialogColor()); + else + rRenderContext.SetLineColor(rStyleSettings.GetShadowColor()); + rRenderContext.DrawLine(Point(1, aRect.Bottom() - 1), Point(aRect.Right() - 1, aRect.Bottom() - 1)); + rRenderContext.DrawLine(Point(aRect.Right() - 1, aRect.Top() + nTopOff), Point(aRect.Right() - 1, aRect.Bottom() - 1)); + if (bNoTabPage) + rRenderContext.SetLineColor(rStyleSettings.GetDialogColor()); + else + rRenderContext.SetLineColor(rStyleSettings.GetDarkShadowColor()); + rRenderContext.DrawLine(Point(0, aRect.Bottom()), Point(aRect.Right(), aRect.Bottom())); + rRenderContext.DrawLine(Point(aRect.Right(), aRect.Top() + nTopOff), Point(aRect.Right(), aRect.Bottom())); + } + else + { + rRenderContext.DrawLine(aRect.TopRight(), aRect.BottomRight()); + rRenderContext.DrawLine(aRect.BottomLeft(), aRect.BottomRight()); + } + } + + if (mbShowTabs && !mpTabCtrlData->maItemList.empty() && mpTabCtrlData->mpListBox == nullptr) + { + // Some native toolkits (GTK+) draw tabs right-to-left, with an + // overlap between adjacent tabs + bool bDrawTabsRTL = rRenderContext.IsNativeControlSupported(ControlType::TabItem, ControlPart::TabsDrawRtl); + ImplTabItem* pFirstTab = nullptr; + ImplTabItem* pLastTab = nullptr; + size_t idx; + + // Even though there is a tab overlap with GTK+, the first tab is not + // overlapped on the left side. Other toolkits ignore this option. + if (bDrawTabsRTL) + { + pFirstTab = mpTabCtrlData->maItemList.data(); + pLastTab = pFirstTab + mpTabCtrlData->maItemList.size() - 1; + idx = mpTabCtrlData->maItemList.size() - 1; + } + else + { + pLastTab = mpTabCtrlData->maItemList.data(); + pFirstTab = pLastTab + mpTabCtrlData->maItemList.size() - 1; + idx = 0; + } + + while (idx < mpTabCtrlData->maItemList.size()) + { + ImplTabItem* pItem = &mpTabCtrlData->maItemList[idx]; + + if (pItem != pCurItem && pItem->m_bVisible && lcl_canPaint(rRenderContext, rRect, pItem->maRect)) + ImplDrawItem(rRenderContext, pItem, aCurRect, pItem == pFirstTab, pItem == pLastTab); + + if (bDrawTabsRTL) + idx--; + else + idx++; + } + + if (pCurItem && lcl_canPaint(rRenderContext, rRect, pCurItem->maRect)) + ImplDrawItem(rRenderContext, pCurItem, aCurRect, pCurItem == pFirstTab, pCurItem == pLastTab); + } + + if (HasFocus()) + ImplShowFocus(); + + mbSmallInvalidate = true; +} + +void TabControl::setAllocation(const Size &rAllocation) +{ + ImplFreeLayoutData(); + + if ( !IsReallyShown() ) + return; + + if( mpTabCtrlData->mpListBox ) + { + // get the listbox' preferred size + Size aTabCtrlSize( GetSizePixel() ); + tools::Long nPrefWidth = mpTabCtrlData->mpListBox->get_preferred_size().Width(); + if( nPrefWidth > aTabCtrlSize.Width() ) + nPrefWidth = aTabCtrlSize.Width(); + Size aNewSize( nPrefWidth, LogicToPixel( Size( 12, 12 ), MapMode( MapUnit::MapAppFont ) ).Height() ); + Point aNewPos( (aTabCtrlSize.Width() - nPrefWidth) / 2, 0 ); + mpTabCtrlData->mpListBox->SetPosSizePixel( aNewPos, aNewSize ); + } + + mbFormat = true; + + // resize/position active TabPage + bool bTabPage = ImplPosCurTabPage(); + + // check what needs to be invalidated + Size aNewSize = rAllocation; + tools::Long nNewWidth = aNewSize.Width(); + for (auto const& item : mpTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + if (!item.mbFullVisible || (item.maRect.Right()-2 >= nNewWidth)) + { + mbSmallInvalidate = false; + break; + } + } + + if ( mbSmallInvalidate ) + { + tools::Rectangle aRect = ImplGetTabRect( TAB_PAGERECT ); + aRect.AdjustLeft( -(TAB_OFFSET+TAB_BORDER_LEFT) ); + aRect.AdjustTop( -(TAB_OFFSET+TAB_BORDER_TOP) ); + aRect.AdjustRight(TAB_OFFSET+TAB_BORDER_RIGHT ); + aRect.AdjustBottom(TAB_OFFSET+TAB_BORDER_BOTTOM ); + if ( bTabPage ) + Invalidate( aRect, InvalidateFlags::NoChildren ); + else + Invalidate( aRect ); + + } + else + { + if ( bTabPage ) + Invalidate( InvalidateFlags::NoChildren ); + else + Invalidate(); + } + + mbLayoutDirty = false; +} + +void TabControl::SetPosSizePixel(const Point& rNewPos, const Size& rNewSize) +{ + Window::SetPosSizePixel(rNewPos, rNewSize); + //if size changed, TabControl::Resize got called already + if (mbLayoutDirty) + setAllocation(rNewSize); +} + +void TabControl::SetSizePixel(const Size& rNewSize) +{ + Window::SetSizePixel(rNewSize); + //if size changed, TabControl::Resize got called already + if (mbLayoutDirty) + setAllocation(rNewSize); +} + +void TabControl::SetPosPixel(const Point& rPos) +{ + Window::SetPosPixel(rPos); + if (mbLayoutDirty) + setAllocation(GetOutputSizePixel()); +} + +void TabControl::Resize() +{ + setAllocation(Control::GetOutputSizePixel()); +} + +void TabControl::GetFocus() +{ + if( ! mpTabCtrlData->mpListBox ) + { + if (mbShowTabs) + { + ImplShowFocus(); + SetInputContext( InputContext( GetFont() ) ); + } + else + { + // no tabs, focus first thing in current page + ImplTabItem* pItem = ImplGetItem(GetCurPageId()); + if (pItem && pItem->mpTabPage) + { + vcl::Window* pFirstChild = pItem->mpTabPage->ImplGetDlgWindow(0, GetDlgWindowType::First); + if ( pFirstChild ) + pFirstChild->ImplControlFocus(GetFocusFlags::Init); + } + } + } + else + { + if( mpTabCtrlData->mpListBox->IsReallyVisible() ) + mpTabCtrlData->mpListBox->GrabFocus(); + } + + Control::GetFocus(); +} + +void TabControl::LoseFocus() +{ + if( mpTabCtrlData && ! mpTabCtrlData->mpListBox ) + HideFocus(); + Control::LoseFocus(); +} + +void TabControl::RequestHelp( const HelpEvent& rHEvt ) +{ + sal_uInt16 nItemId = rHEvt.KeyboardActivated() ? mnCurPageId : GetPageId( ScreenToOutputPixel( rHEvt.GetMousePosPixel() ) ); + + if ( nItemId ) + { + if ( rHEvt.GetMode() & HelpEventMode::BALLOON ) + { + OUString aStr = GetHelpText( nItemId ); + if ( !aStr.isEmpty() ) + { + tools::Rectangle aItemRect = ImplGetTabRect( GetPagePos( nItemId ) ); + Point aPt = OutputToScreenPixel( aItemRect.TopLeft() ); + aItemRect.SetLeft( aPt.X() ); + aItemRect.SetTop( aPt.Y() ); + aPt = OutputToScreenPixel( aItemRect.BottomRight() ); + aItemRect.SetRight( aPt.X() ); + aItemRect.SetBottom( aPt.Y() ); + Help::ShowBalloon( this, aItemRect.Center(), aItemRect, aStr ); + return; + } + } + + // for Quick or Ballon Help, we show the text, if it is cut + if ( rHEvt.GetMode() & (HelpEventMode::QUICK | HelpEventMode::BALLOON) ) + { + ImplTabItem* pItem = ImplGetItem( nItemId ); + const OUString& rStr = pItem->maText; + if ( rStr != pItem->maFormatText ) + { + tools::Rectangle aItemRect = ImplGetTabRect( GetPagePos( nItemId ) ); + Point aPt = OutputToScreenPixel( aItemRect.TopLeft() ); + aItemRect.SetLeft( aPt.X() ); + aItemRect.SetTop( aPt.Y() ); + aPt = OutputToScreenPixel( aItemRect.BottomRight() ); + aItemRect.SetRight( aPt.X() ); + aItemRect.SetBottom( aPt.Y() ); + if ( !rStr.isEmpty() ) + { + if ( rHEvt.GetMode() & HelpEventMode::BALLOON ) + Help::ShowBalloon( this, aItemRect.Center(), aItemRect, rStr ); + else + Help::ShowQuickHelp( this, aItemRect, rStr ); + return; + } + } + } + + if ( rHEvt.GetMode() & HelpEventMode::QUICK ) + { + ImplTabItem* pItem = ImplGetItem( nItemId ); + const OUString& rHelpText = pItem->maHelpText; + if (!rHelpText.isEmpty()) + { + tools::Rectangle aItemRect = ImplGetTabRect( GetPagePos( nItemId ) ); + Point aPt = OutputToScreenPixel( aItemRect.TopLeft() ); + aItemRect.SetLeft( aPt.X() ); + aItemRect.SetTop( aPt.Y() ); + aPt = OutputToScreenPixel( aItemRect.BottomRight() ); + aItemRect.SetRight( aPt.X() ); + aItemRect.SetBottom( aPt.Y() ); + Help::ShowQuickHelp( this, aItemRect, rHelpText ); + return; + } + } + } + + Control::RequestHelp( rHEvt ); +} + +void TabControl::Command( const CommandEvent& rCEvt ) +{ + if( (mpTabCtrlData->mpListBox == nullptr) && (rCEvt.GetCommand() == CommandEventId::ContextMenu) && (GetPageCount() > 1) ) + { + Point aMenuPos; + bool bMenu; + if ( rCEvt.IsMouseEvent() ) + { + aMenuPos = rCEvt.GetMousePosPixel(); + bMenu = GetPageId( aMenuPos ) != 0; + } + else + { + aMenuPos = ImplGetTabRect( GetPagePos( mnCurPageId ) ).Center(); + bMenu = true; + } + + if ( bMenu ) + { + ScopedVclPtrInstance<PopupMenu> aMenu; + for (auto const& item : mpTabCtrlData->maItemList) + { + aMenu->InsertItem(item.id(), item.maText, MenuItemBits::CHECKABLE | MenuItemBits::RADIOCHECK); + if (item.id() == mnCurPageId) + aMenu->CheckItem(item.id()); + aMenu->SetHelpId(item.id(), OString()); + } + + sal_uInt16 nId = aMenu->Execute( this, aMenuPos ); + if ( nId && (nId != mnCurPageId) ) + SelectTabPage( nId ); + return; + } + } + + Control::Command( rCEvt ); +} + +void TabControl::StateChanged( StateChangedType nType ) +{ + Control::StateChanged( nType ); + + if ( nType == StateChangedType::InitShow ) + { + ImplPosCurTabPage(); + if( mpTabCtrlData->mpListBox ) + Resize(); + } + else if ( nType == StateChangedType::UpdateMode ) + { + if ( IsUpdateMode() ) + Invalidate(); + } + else if ( (nType == StateChangedType::Zoom) || + (nType == StateChangedType::ControlFont) ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +void TabControl::DataChanged( const DataChangedEvent& rDCEvt ) +{ + Control::DataChanged( rDCEvt ); + + if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) || + (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) || + ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) ) + { + ImplInitSettings( true ); + Invalidate(); + } +} + +ImplTabItem* TabControl::ImplGetItem(const Point& rPt) const +{ + ImplTabItem* pFoundItem = nullptr; + int nFound = 0; + for (auto & item : mpTabCtrlData->maItemList) + { + if (item.m_bVisible && item.maRect.Contains(rPt)) + { + nFound++; + pFoundItem = &item; + } + } + + // assure that only one tab is highlighted at a time + assert(nFound <= 1); + return nFound == 1 ? pFoundItem : nullptr; +} + +bool TabControl::PreNotify( NotifyEvent& rNEvt ) +{ + if( rNEvt.GetType() == MouseNotifyEvent::MOUSEMOVE ) + { + const MouseEvent* pMouseEvt = rNEvt.GetMouseEvent(); + if( pMouseEvt && !pMouseEvt->GetButtons() && !pMouseEvt->IsSynthetic() && !pMouseEvt->IsModifierChanged() ) + { + // trigger redraw if mouse over state has changed + if( IsNativeControlSupported(ControlType::TabItem, ControlPart::Entire) ) + { + ImplTabItem *pItem = ImplGetItem(GetPointerPosPixel()); + ImplTabItem *pLastItem = ImplGetItem(GetLastPointerPosPixel()); + if ((pItem != pLastItem) || pMouseEvt->IsLeaveWindow() || pMouseEvt->IsEnterWindow()) + { + vcl::Region aClipRgn; + if (pLastItem) + { + // allow for slightly bigger tabitems + // as used by gtk + // TODO: query for the correct sizes + tools::Rectangle aRect(pLastItem->maRect); + aRect.AdjustLeft( -2 ); + aRect.AdjustRight(2 ); + aRect.AdjustTop( -3 ); + aClipRgn.Union( aRect ); + } + + if (pItem) + { + // allow for slightly bigger tabitems + // as used by gtk + // TODO: query for the correct sizes + tools::Rectangle aRect(pItem->maRect); + aRect.AdjustLeft( -2 ); + aRect.AdjustRight(2 ); + aRect.AdjustTop( -3 ); + aClipRgn.Union( aRect ); + } + + if( !aClipRgn.IsEmpty() ) + Invalidate( aClipRgn ); + } + } + } + } + + return Control::PreNotify(rNEvt); +} + +bool TabControl::EventNotify( NotifyEvent& rNEvt ) +{ + bool bRet = false; + + if ( rNEvt.GetType() == MouseNotifyEvent::KEYINPUT ) + bRet = ImplHandleKeyEvent( *rNEvt.GetKeyEvent() ); + + return bRet || Control::EventNotify( rNEvt ); +} + +void TabControl::ActivatePage() +{ + maActivateHdl.Call( this ); +} + +bool TabControl::DeactivatePage() +{ + return !maDeactivateHdl.IsSet() || maDeactivateHdl.Call( this ); +} + +void TabControl::SetTabPageSizePixel( const Size& rSize ) +{ + ImplFreeLayoutData(); + + Size aNewSize( rSize ); + aNewSize.AdjustWidth(TAB_OFFSET*2 ); + tools::Rectangle aRect = ImplGetTabRect( TAB_PAGERECT, + aNewSize.Width(), aNewSize.Height() ); + aNewSize.AdjustHeight(aRect.Top()+TAB_OFFSET ); + Window::SetOutputSizePixel( aNewSize ); +} + +void TabControl::InsertPage( sal_uInt16 nPageId, const OUString& rText, + sal_uInt16 nPos ) +{ + SAL_WARN_IF( !nPageId, "vcl", "TabControl::InsertPage(): PageId == 0" ); + SAL_WARN_IF( GetPagePos( nPageId ) != TAB_PAGE_NOTFOUND, "vcl", + "TabControl::InsertPage(): PageId already exists" ); + + // insert new page item + ImplTabItem* pItem = nullptr; + if( nPos == TAB_APPEND || size_t(nPos) >= mpTabCtrlData->maItemList.size() ) + { + mpTabCtrlData->maItemList.emplace_back(nPageId); + pItem = &mpTabCtrlData->maItemList.back(); + if( mpTabCtrlData->mpListBox ) + mpTabCtrlData->mpListBox->InsertEntry( rText ); + } + else + { + std::vector< ImplTabItem >::iterator new_it = + mpTabCtrlData->maItemList.emplace(mpTabCtrlData->maItemList.begin() + nPos, nPageId); + pItem = &(*new_it); + if( mpTabCtrlData->mpListBox ) + mpTabCtrlData->mpListBox->InsertEntry( rText, nPos); + } + if( mpTabCtrlData->mpListBox ) + { + if( ! mnCurPageId ) + mpTabCtrlData->mpListBox->SelectEntryPos( 0 ); + mpTabCtrlData->mpListBox->SetDropDownLineCount( mpTabCtrlData->mpListBox->GetEntryCount() ); + } + + // set current page id + if ( !mnCurPageId ) + mnCurPageId = nPageId; + + // init new page item + pItem->maText = rText; + pItem->mbFullVisible = false; + + mbFormat = true; + if ( IsUpdateMode() ) + Invalidate(); + + ImplFreeLayoutData(); + if( mpTabCtrlData->mpListBox ) // reposition/resize listbox + Resize(); + + CallEventListeners( VclEventId::TabpageInserted, reinterpret_cast<void*>(nPageId) ); +} + +void TabControl::RemovePage( sal_uInt16 nPageId ) +{ + sal_uInt16 nPos = GetPagePos( nPageId ); + + // does the item exist ? + if ( nPos == TAB_PAGE_NOTFOUND ) + return; + + //remove page item + std::vector< ImplTabItem >::iterator it = mpTabCtrlData->maItemList.begin() + nPos; + bool bIsCurrentPage = (it->id() == mnCurPageId); + mpTabCtrlData->maItemList.erase( it ); + if( mpTabCtrlData->mpListBox ) + { + mpTabCtrlData->mpListBox->RemoveEntry( nPos ); + mpTabCtrlData->mpListBox->SetDropDownLineCount( mpTabCtrlData->mpListBox->GetEntryCount() ); + } + + // If current page is removed, then first page gets the current page + if ( bIsCurrentPage ) + { + mnCurPageId = 0; + + if( ! mpTabCtrlData->maItemList.empty() ) + { + // don't do this by simply setting mnCurPageId to pFirstItem->id() + // this leaves a lot of stuff (such trivia as _showing_ the new current page) undone + // instead, call SetCurPageId + // without this, the next (outside) call to SetCurPageId with the id of the first page + // will result in doing nothing (as we assume that nothing changed, then), and the page + // will never be shown. + // 86875 - 05/11/2001 - frank.schoenheit@germany.sun.com + + SetCurPageId(mpTabCtrlData->maItemList[0].id()); + } + } + + mbFormat = true; + if ( IsUpdateMode() ) + Invalidate(); + + ImplFreeLayoutData(); + + CallEventListeners( VclEventId::TabpageRemoved, reinterpret_cast<void*>(nPageId) ); +} + +void TabControl::SetPageEnabled( sal_uInt16 i_nPageId, bool i_bEnable ) +{ + ImplTabItem* pItem = ImplGetItem( i_nPageId ); + + if (!pItem || pItem->m_bEnabled == i_bEnable) + return; + + pItem->m_bEnabled = i_bEnable; + if (!pItem->m_bVisible) + return; + + mbFormat = true; + if( mpTabCtrlData->mpListBox ) + mpTabCtrlData->mpListBox->SetEntryFlags( GetPagePos( i_nPageId ), + i_bEnable ? ListBoxEntryFlags::NONE : (ListBoxEntryFlags::DisableSelection | ListBoxEntryFlags::DrawDisabled) ); + + // SetCurPageId will change to a valid page + if (pItem->id() == mnCurPageId) + SetCurPageId( mnCurPageId ); + else if ( IsUpdateMode() ) + Invalidate(); +} + +void TabControl::SetPageVisible( sal_uInt16 nPageId, bool bVisible ) +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + if (!pItem || pItem->m_bVisible == bVisible) + return; + + pItem->m_bVisible = bVisible; + if (!bVisible) + { + if (pItem->mbFullVisible) + mbSmallInvalidate = false; + pItem->mbFullVisible = false; + pItem->maRect.SetEmpty(); + } + mbFormat = true; + + // SetCurPageId will change to a valid page + if (pItem->id() == mnCurPageId) + SetCurPageId(mnCurPageId); + else if (IsUpdateMode()) + Invalidate(); +} + +sal_uInt16 TabControl::GetPageCount() const +{ + return static_cast<sal_uInt16>(mpTabCtrlData->maItemList.size()); +} + +sal_uInt16 TabControl::GetPageId( sal_uInt16 nPos ) const +{ + if( size_t(nPos) < mpTabCtrlData->maItemList.size() ) + return mpTabCtrlData->maItemList[nPos].id(); + return 0; +} + +sal_uInt16 TabControl::GetPagePos( sal_uInt16 nPageId ) const +{ + sal_uInt16 nPos = 0; + for (auto const& item : mpTabCtrlData->maItemList) + { + if (item.id() == nPageId) + return nPos; + ++nPos; + } + + return TAB_PAGE_NOTFOUND; +} + +sal_uInt16 TabControl::GetPageId( const Point& rPos ) const +{ + Size winSize = Control::GetOutputSizePixel(); + const auto &rList = mpTabCtrlData->maItemList; + const auto it = std::find_if(rList.begin(), rList.end(), [&rPos, &winSize, this](const auto &item) { + return const_cast<TabControl*>(this)->ImplGetTabRect(&item, winSize.Width(), winSize.Height()).Contains(rPos); }); + return (it != rList.end()) ? it->id() : 0; +} + +sal_uInt16 TabControl::GetPageId( const OString& rName ) const +{ + const auto &rList = mpTabCtrlData->maItemList; + const auto it = std::find_if(rList.begin(), rList.end(), [&rName](const auto &item) { + return item.maTabName == rName; }); + return (it != rList.end()) ? it->id() : 0; +} + +void TabControl::SetCurPageId( sal_uInt16 nPageId ) +{ + sal_uInt16 nPos = GetPagePos( nPageId ); + while (nPos != TAB_PAGE_NOTFOUND && !mpTabCtrlData->maItemList[nPos].m_bEnabled) + { + nPos++; + if( size_t(nPos) >= mpTabCtrlData->maItemList.size() ) + nPos = 0; + if (mpTabCtrlData->maItemList[nPos].id() == nPageId) + break; + } + + if( nPos == TAB_PAGE_NOTFOUND ) + return; + + nPageId = mpTabCtrlData->maItemList[nPos].id(); + if ( nPageId == mnCurPageId ) + { + if ( mnActPageId ) + mnActPageId = nPageId; + return; + } + + if ( mnActPageId ) + mnActPageId = nPageId; + else + { + mbFormat = true; + sal_uInt16 nOldId = mnCurPageId; + mnCurPageId = nPageId; + ImplChangeTabPage( nPageId, nOldId ); + } +} + +sal_uInt16 TabControl::GetCurPageId() const +{ + if ( mnActPageId ) + return mnActPageId; + else + return mnCurPageId; +} + +void TabControl::SelectTabPage( sal_uInt16 nPageId ) +{ + if ( !nPageId || (nPageId == mnCurPageId) ) + return; + + ImplFreeLayoutData(); + + CallEventListeners( VclEventId::TabpageDeactivate, reinterpret_cast<void*>(mnCurPageId) ); + if ( DeactivatePage() ) + { + mnActPageId = nPageId; + ActivatePage(); + // Page could have been switched by the Activate handler + nPageId = mnActPageId; + mnActPageId = 0; + SetCurPageId( nPageId ); + if( mpTabCtrlData->mpListBox ) + mpTabCtrlData->mpListBox->SelectEntryPos( GetPagePos( nPageId ) ); + CallEventListeners( VclEventId::TabpageActivate, reinterpret_cast<void*>(nPageId) ); + } +} + +void TabControl::SetTabPage( sal_uInt16 nPageId, TabPage* pTabPage ) +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + + if ( !pItem || (pItem->mpTabPage.get() == pTabPage) ) + return; + + if ( pTabPage ) + { + if ( IsDefaultSize() ) + SetTabPageSizePixel( pTabPage->GetSizePixel() ); + + // only set here, so that Resize does not reposition TabPage + pItem->mpTabPage = pTabPage; + queue_resize(); + + if (pItem->id() == mnCurPageId) + ImplChangeTabPage(pItem->id(), 0); + } + else + { + pItem->mpTabPage = nullptr; + queue_resize(); + } +} + +TabPage* TabControl::GetTabPage( sal_uInt16 nPageId ) const +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + + if ( pItem ) + return pItem->mpTabPage; + else + return nullptr; +} + +void TabControl::SetPageText( sal_uInt16 nPageId, const OUString& rText ) +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + + if ( !pItem || pItem->maText == rText ) + return; + + pItem->maText = rText; + mbFormat = true; + if( mpTabCtrlData->mpListBox ) + { + sal_uInt16 nPos = GetPagePos( nPageId ); + mpTabCtrlData->mpListBox->RemoveEntry( nPos ); + mpTabCtrlData->mpListBox->InsertEntry( rText, nPos ); + } + if ( IsUpdateMode() ) + Invalidate(); + ImplFreeLayoutData(); + CallEventListeners( VclEventId::TabpagePageTextChanged, reinterpret_cast<void*>(nPageId) ); +} + +OUString const & TabControl::GetPageText( sal_uInt16 nPageId ) const +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + + assert( pItem ); + + return pItem->maText; +} + +void TabControl::SetHelpText( sal_uInt16 nPageId, const OUString& rText ) +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + + assert( pItem ); + + pItem->maHelpText = rText; +} + +const OUString& TabControl::GetHelpText( sal_uInt16 nPageId ) const +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + assert( pItem ); + return pItem->maHelpText; +} + +void TabControl::SetAccessibleName(sal_uInt16 nPageId, const OUString& rName) +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + assert( pItem ); + pItem->maAccessibleName = rName; +} + +OUString TabControl::GetAccessibleName( sal_uInt16 nPageId ) const +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + assert( pItem ); + if (!pItem->maAccessibleName.isEmpty()) + return pItem->maAccessibleName; + return OutputDevice::GetNonMnemonicString(pItem->maText); +} + +void TabControl::SetAccessibleDescription(sal_uInt16 nPageId, const OUString& rDesc) +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + assert( pItem ); + pItem->maAccessibleDescription = rDesc; +} + +OUString TabControl::GetAccessibleDescription( sal_uInt16 nPageId ) const +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + assert( pItem ); + if (!pItem->maAccessibleDescription.isEmpty()) + return pItem->maAccessibleDescription; + return pItem->maHelpText; +} + +void TabControl::SetPageName( sal_uInt16 nPageId, const OString& rName ) const +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + + if ( pItem ) + pItem->maTabName = rName; +} + +OString TabControl::GetPageName( sal_uInt16 nPageId ) const +{ + ImplTabItem* pItem = ImplGetItem( nPageId ); + + if (pItem) + return pItem->maTabName; + + return OString(); +} + +void TabControl::SetPageImage( sal_uInt16 i_nPageId, const Image& i_rImage ) +{ + ImplTabItem* pItem = ImplGetItem( i_nPageId ); + + if ( pItem ) + { + pItem->maTabImage = i_rImage; + mbFormat = true; + if ( IsUpdateMode() ) + Invalidate(); + } +} + +tools::Rectangle TabControl::GetCharacterBounds( sal_uInt16 nPageId, tools::Long nIndex ) const +{ + tools::Rectangle aRet; + + if( !HasLayoutData() || mpTabCtrlData->maLayoutPageIdToLine.empty() ) + FillLayoutData(); + + if( HasLayoutData() ) + { + std::unordered_map< int, int >::const_iterator it = mpTabCtrlData->maLayoutPageIdToLine.find( static_cast<int>(nPageId) ); + if( it != mpTabCtrlData->maLayoutPageIdToLine.end() ) + { + Pair aPair = mxLayoutData->GetLineStartEnd( it->second ); + if( (aPair.B() - aPair.A()) >= nIndex ) + aRet = mxLayoutData->GetCharacterBounds( aPair.A() + nIndex ); + } + } + + return aRet; +} + +tools::Long TabControl::GetIndexForPoint( const Point& rPoint, sal_uInt16& rPageId ) const +{ + tools::Long nRet = -1; + + if( !HasLayoutData() || mpTabCtrlData->maLayoutPageIdToLine.empty() ) + FillLayoutData(); + + if( HasLayoutData() ) + { + int nIndex = mxLayoutData->GetIndexForPoint( rPoint ); + if( nIndex != -1 ) + { + // what line (->pageid) is this index in ? + int nLines = mxLayoutData->GetLineCount(); + int nLine = -1; + while( ++nLine < nLines ) + { + Pair aPair = mxLayoutData->GetLineStartEnd( nLine ); + if( aPair.A() <= nIndex && aPair.B() >= nIndex ) + { + nRet = nIndex - aPair.A(); + rPageId = static_cast<sal_uInt16>(mpTabCtrlData->maLayoutLineToPageId[ nLine ]); + break; + } + } + } + } + + return nRet; +} + +void TabControl::FillLayoutData() const +{ + mpTabCtrlData->maLayoutLineToPageId.clear(); + mpTabCtrlData->maLayoutPageIdToLine.clear(); + const_cast<TabControl*>(this)->Invalidate(); +} + +tools::Rectangle TabControl::GetTabBounds( sal_uInt16 nPageId ) const +{ + tools::Rectangle aRet; + + ImplTabItem* pItem = ImplGetItem( nPageId ); + if (pItem && pItem->m_bVisible) + aRet = pItem->maRect; + + return aRet; +} + +Size TabControl::ImplCalculateRequisition(sal_uInt16& nHeaderHeight) const +{ + Size aOptimalPageSize(0, 0); + + sal_uInt16 nOrigPageId = GetCurPageId(); + for (auto const& item : mpTabCtrlData->maItemList) + { + const TabPage *pPage = item.mpTabPage; + //it's a real nuisance if the page is not inserted yet :-( + //We need to force all tabs to exist to get overall optimal size for dialog + if (!pPage) + { + TabControl *pThis = const_cast<TabControl*>(this); + pThis->SetCurPageId(item.id()); + pThis->ActivatePage(); + pPage = item.mpTabPage; + } + + if (!pPage) + continue; + + Size aPageSize(VclContainer::getLayoutRequisition(*pPage)); + + if (aPageSize.Width() > aOptimalPageSize.Width()) + aOptimalPageSize.setWidth( aPageSize.Width() ); + if (aPageSize.Height() > aOptimalPageSize.Height()) + aOptimalPageSize.setHeight( aPageSize.Height() ); + } + + //fdo#61940 If we were forced to activate pages in order to on-demand + //create them to get their optimal size, then switch back to the original + //page and re-activate it + if (nOrigPageId != GetCurPageId()) + { + TabControl *pThis = const_cast<TabControl*>(this); + pThis->SetCurPageId(nOrigPageId); + pThis->ActivatePage(); + } + + tools::Long nTabLabelsBottom = 0, nTabLabelsRight = 0; + if (mbShowTabs) + { + for (sal_uInt16 nPos(0), sizeList(static_cast <sal_uInt16> (mpTabCtrlData->maItemList.size())); + nPos < sizeList; ++nPos) + { + TabControl* pThis = const_cast<TabControl*>(this); + + tools::Rectangle aTabRect = pThis->ImplGetTabRect(nPos, aOptimalPageSize.Width(), LONG_MAX); + if (aTabRect.Bottom() > nTabLabelsBottom) + { + nTabLabelsBottom = aTabRect.Bottom(); + nHeaderHeight = nTabLabelsBottom; + } + if (!aTabRect.IsEmpty() && aTabRect.Right() > nTabLabelsRight) + nTabLabelsRight = aTabRect.Right(); + } + } + + Size aOptimalSize(aOptimalPageSize); + aOptimalSize.AdjustHeight(nTabLabelsBottom ); + aOptimalSize.setWidth( std::max(nTabLabelsRight, aOptimalSize.Width()) ); + + aOptimalSize.AdjustWidth(TAB_OFFSET * 2 ); + aOptimalSize.AdjustHeight(TAB_OFFSET * 2 ); + + return aOptimalSize; +} + +Size TabControl::calculateRequisition() const +{ + sal_uInt16 nHeaderHeight; + return ImplCalculateRequisition(nHeaderHeight); +} + +Size TabControl::GetOptimalSize() const +{ + return calculateRequisition(); +} + +void TabControl::queue_resize(StateChangedType eReason) +{ + mbLayoutDirty = true; + Window::queue_resize(eReason); +} + +std::vector<sal_uInt16> TabControl::GetPageIDs() const +{ + std::vector<sal_uInt16> aIDs; + for (auto const& item : mpTabCtrlData->maItemList) + { + aIDs.push_back(item.id()); + } + + return aIDs; +} + +bool TabControl::set_property(const OString &rKey, const OUString &rValue) +{ + if (rKey == "show-tabs") + { + mbShowTabs = toBool(rValue); + queue_resize(); + } + else + return Control::set_property(rKey, rValue); + return true; +} + +FactoryFunction TabControl::GetUITestFactory() const +{ + return TabControlUIObject::create; +} + +void TabControl::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + rJsonWriter.put("id", get_id()); + rJsonWriter.put("type", "tabcontrol"); + rJsonWriter.put("selected", GetCurPageId()); + + { + auto childrenNode = rJsonWriter.startArray("children"); + for (int i = 0; i < GetChildCount(); i++) + { + vcl::Window* pChild = GetChild(i); + + if (pChild) + { + auto childNode = rJsonWriter.startStruct(); + pChild->DumpAsPropertyTree(rJsonWriter); + + if (!pChild->IsVisible()) + rJsonWriter.put("hidden", "true"); + } + } + } + { + auto tabsNode = rJsonWriter.startArray("tabs"); + for(auto id : GetPageIDs()) + { + auto tabNode = rJsonWriter.startStruct(); + rJsonWriter.put("text", GetPageText(id)); + rJsonWriter.put("id", id); + rJsonWriter.put("name", GetPageName(id)); + } + } +} + +sal_uInt16 NotebookbarTabControlBase::m_nHeaderHeight = 0; + +IMPL_LINK_NOARG(NotebookbarTabControlBase, OpenMenu, Button*, void) +{ + m_aIconClickHdl.Call(static_cast<NotebookBar*>(GetParent()->GetParent())); +} + +NotebookbarTabControlBase::NotebookbarTabControlBase(vcl::Window* pParent) + : TabControl(pParent, WB_STDTABCONTROL) + , bLastContextWasSupported(true) + , eLastContext(vcl::EnumContext::Context::Any) +{ + m_pOpenMenu = VclPtr<PushButton>::Create( this , WB_CENTER | WB_VCENTER ); + m_pOpenMenu->SetClickHdl(LINK(this, NotebookbarTabControlBase, OpenMenu)); + m_pOpenMenu->SetModeImage(Image(StockImage::Yes, SV_RESID_BITMAP_NOTEBOOKBAR)); + m_pOpenMenu->SetSizePixel(m_pOpenMenu->GetOptimalSize()); + m_pOpenMenu->Show(); +} + +NotebookbarTabControlBase::~NotebookbarTabControlBase() +{ + disposeOnce(); +} + +void NotebookbarTabControlBase::SetContext( vcl::EnumContext::Context eContext ) +{ + if (eLastContext == eContext) + return; + + bool bHandled = false; + + for (int nChild = 0; nChild < GetPageCount(); ++nChild) + { + sal_uInt16 nPageId = TabControl::GetPageId(nChild); + TabPage* pPage = GetTabPage(nPageId); + + if (pPage) + { + SetPageVisible(nPageId, pPage->HasContext(eContext) || pPage->HasContext(vcl::EnumContext::Context::Any)); + + if (!bHandled && bLastContextWasSupported + && pPage->HasContext(vcl::EnumContext::Context::Default)) + { + SetCurPageId(nPageId); + } + + if (pPage->HasContext(eContext) && eContext != vcl::EnumContext::Context::Any) + { + SetCurPageId(nPageId); + bHandled = true; + bLastContextWasSupported = true; + } + } + } + + if (!bHandled) + bLastContextWasSupported = false; + eLastContext = eContext; + + // tdf#152908 Tabbed compact toolbar does not repaint itself when tabs getting removed + // For unknown reason this is needed by the tabbed compact toolbar for other than gtk + // vcl backends. + Resize(); +} + +void NotebookbarTabControlBase::dispose() +{ + m_pShortcuts.disposeAndClear(); + m_pOpenMenu.disposeAndClear(); + TabControl::dispose(); +} + +void NotebookbarTabControlBase::SetToolBox( ToolBox* pToolBox ) +{ + m_pShortcuts.set( pToolBox ); +} + +void NotebookbarTabControlBase::SetIconClickHdl( Link<NotebookBar*, void> aHdl ) +{ + m_aIconClickHdl = aHdl; +} + +static bool lcl_isValidPage(const ImplTabItem& rItem, bool& bFound) +{ + if (rItem.m_bVisible && rItem.m_bEnabled) + bFound = true; + return bFound; +} + +void NotebookbarTabControlBase::ImplActivateTabPage( bool bNext ) +{ + const sal_uInt16 nOldPos = GetPagePos(GetCurPageId()); + bool bFound = false; + sal_Int32 nCurPos = nOldPos; + + if (bNext) + { + for (nCurPos++; nCurPos < GetPageCount(); nCurPos++) + if (lcl_isValidPage(mpTabCtrlData->maItemList[nCurPos], bFound)) + break; + } + else + { + for (nCurPos--; nCurPos >= 0; nCurPos--) + if (lcl_isValidPage(mpTabCtrlData->maItemList[nCurPos], bFound)) + break; + } + + if (!bFound) + nCurPos = nOldPos; + SelectTabPage( TabControl::GetPageId( nCurPos ) ); +} + +sal_uInt16 NotebookbarTabControlBase::GetHeaderHeight() +{ + return m_nHeaderHeight; +} + +bool NotebookbarTabControlBase::ImplPlaceTabs( tools::Long nWidth ) +{ + if ( nWidth <= 0 ) + return false; + if ( mpTabCtrlData->maItemList.empty() ) + return false; + if (!m_pOpenMenu || m_pOpenMenu->isDisposed()) + return false; + + const tools::Long nHamburgerWidth = m_pOpenMenu->GetSizePixel().Width(); + tools::Long nMaxWidth = nWidth - nHamburgerWidth; + tools::Long nShortcutsWidth = m_pShortcuts != nullptr ? m_pShortcuts->GetSizePixel().getWidth() + 1 : 0; + tools::Long nFullWidth = nShortcutsWidth; + + const tools::Long nOffsetX = 2 + nShortcutsWidth; + const tools::Long nOffsetY = 2; + + //fdo#66435 throw Knuth/Tex minimum raggedness algorithm at the problem + //of ugly bare tabs on lines of their own + + for (auto & item : mpTabCtrlData->maItemList) + { + tools::Long nTabWidth = 0; + if (item.m_bVisible) + { + nTabWidth = ImplGetItemSize(&item, nMaxWidth).getWidth(); + if (!item.maText.isEmpty() && nTabWidth < 100) + nTabWidth = 100; + } + nFullWidth += nTabWidth; + } + + tools::Long nX = nOffsetX; + tools::Long nY = nOffsetY; + + tools::Long nLineWidthAry[100]; + nLineWidthAry[0] = 0; + + for (auto & item : mpTabCtrlData->maItemList) + { + if (!item.m_bVisible) + continue; + + Size aSize = ImplGetItemSize( &item, nMaxWidth ); + + // set minimum tab size + if( nFullWidth < nMaxWidth && !item.maText.isEmpty() && aSize.getWidth() < 100) + aSize.setWidth( 100 ); + + if( !item.maText.isEmpty() && aSize.getHeight() < 28 ) + aSize.setHeight( 28 ); + + tools::Rectangle aNewRect( Point( nX, nY ), aSize ); + if ( mbSmallInvalidate && (item.maRect != aNewRect) ) + mbSmallInvalidate = false; + + item.maRect = aNewRect; + item.mnLine = 0; + item.mbFullVisible = true; + + nLineWidthAry[0] += aSize.Width(); + nX += aSize.Width(); + } + + // we always have only one line of tabs + lcl_AdjustSingleLineTabs(nMaxWidth, mpTabCtrlData.get()); + + // position the shortcutbox + if (m_pShortcuts) + { + tools::Long nPosY = (m_nHeaderHeight - m_pShortcuts->GetSizePixel().getHeight()) / 2; + m_pShortcuts->SetPosPixel(Point(0, nPosY)); + } + + tools::Long nPosY = (m_nHeaderHeight - m_pOpenMenu->GetSizePixel().getHeight()) / 2; + // position the menu + m_pOpenMenu->SetPosPixel(Point(nWidth - nHamburgerWidth, nPosY)); + + return true; +} + +Size NotebookbarTabControlBase::calculateRequisition() const +{ + return TabControl::ImplCalculateRequisition(m_nHeaderHeight); +} + +Control* NotebookbarTabControlBase::GetOpenMenu() +{ + return m_pOpenMenu; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/throbber.cxx b/vcl/source/control/throbber.cxx new file mode 100644 index 000000000..d7dc3e4a7 --- /dev/null +++ b/vcl/source/control/throbber.cxx @@ -0,0 +1,235 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/toolkit/throbber.hxx> +#include <vcl/svapp.hxx> + +#include <com/sun/star/uno/XComponentContext.hpp> +#include <com/sun/star/graphic/GraphicProvider.hpp> +#include <com/sun/star/graphic/XGraphicProvider.hpp> +#include <com/sun/star/awt/ImageScaleMode.hpp> + +#include <comphelper/namedvaluecollection.hxx> +#include <comphelper/processfactory.hxx> +#include <rtl/ustrbuf.hxx> +#include <sal/log.hxx> +#include <tools/urlobj.hxx> + +#include <limits> + +using ::com::sun::star::uno::Reference; +using ::com::sun::star::graphic::XGraphic; +using ::com::sun::star::graphic::XGraphicProvider; +using ::com::sun::star::uno::Exception; +namespace ImageScaleMode = ::com::sun::star::awt::ImageScaleMode; + +Throbber::Throbber( vcl::Window* i_parentWindow, WinBits i_style ) + :ImageControl( i_parentWindow, i_style ) + ,mbRepeat( true ) + ,mnStepTime( 100 ) + ,mnCurStep( 0 ) + ,maWaitTimer("Throbber maWaitTimer") +{ + maWaitTimer.SetTimeout( mnStepTime ); + maWaitTimer.SetInvokeHandler( LINK( this, Throbber, TimeOutHdl ) ); + + SetScaleMode( ImageScaleMode::NONE ); + initImages(); +} + +Throbber::~Throbber() +{ + disposeOnce(); +} + +void Throbber::dispose() +{ + maWaitTimer.Stop(); + ImageControl::dispose(); +} + +namespace +{ + ::std::vector< Image > lcl_loadImageSet( const Throbber::ImageSet i_imageSet ) + { + ::std::vector< Image > aImages; + + const Reference< css::uno::XComponentContext > aContext( ::comphelper::getProcessComponentContext() ); + const Reference< XGraphicProvider > xGraphicProvider( css::graphic::GraphicProvider::create(aContext) ); + + ::std::vector< OUString > aImageURLs( Throbber::getDefaultImageURLs( i_imageSet ) ); + aImages.reserve( aImageURLs.size() ); + + ::comphelper::NamedValueCollection aMediaProperties; + for ( const auto& rImageURL : aImageURLs ) + { + Reference< XGraphic > xGraphic; + aMediaProperties.put( "URL", rImageURL ); + xGraphic = xGraphicProvider->queryGraphic( aMediaProperties.getPropertyValues() ); + aImages.emplace_back( xGraphic ); + } + + return aImages; + } +} + +void Throbber::Resize() +{ + ImageControl::Resize(); + initImages(); +} + +void Throbber::initImages() +{ + try + { + ::std::vector< ::std::vector< Image > > aImageSets + { + lcl_loadImageSet( ImageSet::N16px ), + lcl_loadImageSet( ImageSet::N32px ), + lcl_loadImageSet( ImageSet::N64px ) + }; + + // find the best matching image set (size-wise) + const ::Size aWindowSizePixel = GetSizePixel(); + size_t nPreferredSet = 0; + if ( aImageSets.size() > 1 ) + { + tools::Long nMinimalDistance = ::std::numeric_limits< tools::Long >::max(); + for ( ::std::vector< ::std::vector< Image > >::const_iterator check = aImageSets.begin(); + check != aImageSets.end(); + ++check + ) + { + if ( check->empty() ) + { + SAL_WARN( "vcl.control", "Throbber::initImages: illegal image!" ); + continue; + } + + const Size aImageSize = (*check)[0].GetSizePixel(); + + if ( ( aImageSize.Width() > aWindowSizePixel.Width() ) + || ( aImageSize.Height() > aWindowSizePixel.Height() ) + ) + // do not use an image set which doesn't fit into the window + continue; + + const sal_Int64 distance = + ( aWindowSizePixel.Width() - aImageSize.Width() ) * ( aWindowSizePixel.Width() - aImageSize.Width() ) + + ( aWindowSizePixel.Height() - aImageSize.Height() ) * ( aWindowSizePixel.Height() - aImageSize.Height() ); + if ( distance < nMinimalDistance ) + { + nMinimalDistance = distance; + nPreferredSet = check - aImageSets.begin(); + } + } + } + + if ( nPreferredSet < aImageSets.size() ) + setImageList( std::vector(aImageSets[nPreferredSet]) ); + } + catch( const Exception& ) + { + } +} + +void Throbber::start() +{ + maWaitTimer.Start(); +} + +void Throbber::stop() +{ + maWaitTimer.Stop(); +} + +bool Throbber::isRunning() const +{ + return maWaitTimer.IsActive(); +} + +void Throbber::setImageList( ::std::vector< Image > && i_images ) +{ + SAL_WARN_IF( i_images.size()>=SAL_MAX_INT32, "vcl.control", "Throbber::setImageList: too many images!" ); + + maImageList = std::move(i_images); + + const Image aInitialImage( !maImageList.empty() ? maImageList[ 0 ] : Image() ); + SetImage( aInitialImage ); +} + +::std::vector< OUString > Throbber::getDefaultImageURLs( const ImageSet i_imageSet ) +{ + ::std::vector< OUString > aImageURLs; + + char const* const pResolutions[] = { "16", "32", "64" }; + size_t const nImageCounts[] = { 6, 12, 12 }; + + size_t index = 0; + switch ( i_imageSet ) + { + case ImageSet::N16px: index = 0; break; + case ImageSet::N32px: index = 1; break; + case ImageSet::N64px: index = 2; break; + } + + aImageURLs.reserve( nImageCounts[index] ); + for ( size_t i=0; i<nImageCounts[index]; ++i ) + { + OUStringBuffer aURL; + aURL.append( "private:graphicrepository/vcl/res/spinner-" ); + aURL.appendAscii( pResolutions[index] ); + aURL.append( "-" ); + if ( i < 9 ) + aURL.append( "0" ); + aURL.append ( sal_Int32( i + 1 ) ); + aURL.append( ".png" ); + + aImageURLs.push_back( aURL.makeStringAndClear() ); + } + + return aImageURLs; +} + +IMPL_LINK_NOARG(Throbber, TimeOutHdl, Timer *, void) +{ + SolarMutexGuard aGuard; + if ( maImageList.empty() ) + return; + + if ( mnCurStep < static_cast<sal_Int32>(maImageList.size()-1) ) + ++mnCurStep; + else + { + if ( mbRepeat ) + { + // start over + mnCurStep = 0; + } + else + { + stop(); + } + } + + SetImage( maImageList[ mnCurStep ] ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/thumbpos.hxx b/vcl/source/control/thumbpos.hxx new file mode 100644 index 000000000..e7048f35f --- /dev/null +++ b/vcl/source/control/thumbpos.hxx @@ -0,0 +1,24 @@ +/* -*- 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/. + */ + +#ifndef INCLUDED_VCL_SOURCE_CONTROL_THUMBPOS_HXX +#define INCLUDED_VCL_SOURCE_CONTROL_THUMBPOS_HXX + +inline tools::Long ImplMulDiv(tools::Long nNumber, tools::Long nNumerator, tools::Long nDenominator) +{ + if (!nDenominator) + return 0; + double n = (static_cast<double>(nNumber) * static_cast<double>(nNumerator)) + / static_cast<double>(nDenominator); + return static_cast<tools::Long>(n); +} + +#endif // INCLUDED_VCL_SOURCE_CONTROL_THUMBPOS_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/wizardmachine.cxx b/vcl/source/control/wizardmachine.cxx new file mode 100644 index 000000000..e2ed41ff0 --- /dev/null +++ b/vcl/source/control/wizardmachine.cxx @@ -0,0 +1,1418 @@ +/* -*- 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 <comphelper/lok.hxx> +#include <officecfg/Office/Common.hxx> +#include <vcl/event.hxx> +#include <tools/debug.hxx> +#include <tools/diagnose_ex.h> +#include <strings.hrc> +#include <svdata.hxx> +#include <wizdlg.hxx> +#include <stack> +#include "wizimpldata.hxx" + +constexpr OStringLiteral HID_WIZARD_NEXT = "SVT_HID_WIZARD_NEXT"; +constexpr OStringLiteral HID_WIZARD_PREVIOUS = "SVT_HID_WIZARD_PREVIOUS"; + +#define WIZARDDIALOG_BUTTON_OFFSET_Y 6 +#define WIZARDDIALOG_BUTTON_DLGOFFSET_X 6 +#define WIZARDDIALOG_VIEW_DLGOFFSET_X 6 +#define WIZARDDIALOG_VIEW_DLGOFFSET_Y 6 + +namespace vcl +{ + //= WizardPageImplData + OWizardPage::OWizardPage(weld::Container* pPage, weld::DialogController* pController, const OUString& rUIXMLDescription, const OString& rID) + : BuilderPage(pPage, pController, rUIXMLDescription, rID) + { + } + + OWizardPage::~OWizardPage() + { + } + + void OWizardPage::initializePage() + { + } + + void OWizardPage::Activate() + { + BuilderPage::Activate(); + updateDialogTravelUI(); + } + + void OWizardPage::updateDialogTravelUI() + { + auto pWizardMachine = dynamic_cast<RoadmapWizardMachine*>(m_pDialogController); + if (pWizardMachine) + pWizardMachine->updateTravelUI(); + } + + bool OWizardPage::canAdvance() const + { + return true; + } + + bool OWizardPage::commitPage( WizardTypes::CommitPageReason ) + { + return true; + } + + void RoadmapWizard::SetLeftAlignedButtonCount( sal_Int16 _nCount ) + { + mnLeftAlignCount = _nCount; + } + + void RoadmapWizard::ImplCalcSize( Size& rSize ) + { + // calculate ButtonBar height and width + tools::Long nMaxHeight = 0; + tools::Long nBarWidth = WIZARDDIALOG_BUTTON_DLGOFFSET_X * 2 + LogicalCoordinateToPixel(6); + ImplWizButtonData* pBtnData = mpFirstBtn; + while (pBtnData) + { + auto nBtnHeight = pBtnData->mpButton->GetSizePixel().Height(); + auto nBtnWidth = pBtnData->mpButton->GetSizePixel().Width(); + if (pBtnData->mpButton->IsVisible()) + { + nBarWidth += nBtnWidth; + nBarWidth += pBtnData->mnOffset; + } + if ( nBtnHeight > nMaxHeight ) + nMaxHeight = nBtnHeight; + pBtnData = pBtnData->mpNext; + } + if ( nMaxHeight ) + nMaxHeight += WIZARDDIALOG_BUTTON_OFFSET_Y*2; + rSize.AdjustHeight(nMaxHeight); + + // add in the view window size + if ( mpViewWindow && mpViewWindow->IsVisible() ) + { + Size aViewSize = mpViewWindow->GetSizePixel(); + // align left + rSize.AdjustWidth(aViewSize.Width() ); + } + + if (nBarWidth > rSize.Width()) + rSize.setWidth(nBarWidth); + } + + void RoadmapWizard::queue_resize(StateChangedType /*eReason*/) + { + if (maWizardLayoutIdle.IsActive()) + return; + if (IsInClose()) + return; + maWizardLayoutIdle.Start(); + } + + IMPL_LINK_NOARG(RoadmapWizard, ImplHandleWizardLayoutTimerHdl, Timer*, void) + { + ImplPosCtrls(); + ImplPosTabPage(); + } + + void RoadmapWizard::ImplPosCtrls() + { + Size aDlgSize = GetOutputSizePixel(); + tools::Long nBtnWidth = 0; + tools::Long nMaxHeight = 0; + tools::Long nOffY = aDlgSize.Height(); + + ImplWizButtonData* pBtnData = mpFirstBtn; + int j = 0; + while ( pBtnData ) + { + if (j >= mnLeftAlignCount) + { + Size aBtnSize = pBtnData->mpButton->GetSizePixel(); + tools::Long nBtnHeight = aBtnSize.Height(); + if ( nBtnHeight > nMaxHeight ) + nMaxHeight = nBtnHeight; + nBtnWidth += aBtnSize.Width(); + nBtnWidth += pBtnData->mnOffset; + } + pBtnData = pBtnData->mpNext; + j++; + } + + if ( nMaxHeight ) + { + tools::Long nOffX = aDlgSize.Width()-nBtnWidth-WIZARDDIALOG_BUTTON_DLGOFFSET_X; + tools::Long nOffLeftAlignX = LogicalCoordinateToPixel(6); + nOffY -= WIZARDDIALOG_BUTTON_OFFSET_Y+nMaxHeight; + + pBtnData = mpFirstBtn; + int i = 0; + while ( pBtnData ) + { + Size aBtnSize = pBtnData->mpButton->GetSizePixel(); + if (i >= mnLeftAlignCount) + { + Point aPos( nOffX, nOffY+((nMaxHeight-aBtnSize.Height())/2) ); + pBtnData->mpButton->SetPosPixel( aPos ); + nOffX += aBtnSize.Width(); + nOffX += pBtnData->mnOffset; + } + else + { + Point aPos( nOffLeftAlignX, nOffY+((nMaxHeight-aBtnSize.Height())/2) ); + pBtnData->mpButton->SetPosPixel( aPos ); + nOffLeftAlignX += aBtnSize.Width(); + nOffLeftAlignX += pBtnData->mnOffset; + } + + pBtnData = pBtnData->mpNext; + i++; + } + + nOffY -= WIZARDDIALOG_BUTTON_OFFSET_Y; + } + + if ( !(mpViewWindow && mpViewWindow->IsVisible()) ) + return; + + tools::Long nViewOffX = 0; + tools::Long nViewOffY = 0; + tools::Long nViewWidth = 0; + tools::Long nViewHeight = 0; + tools::Long nDlgHeight = nOffY; + PosSizeFlags nViewPosFlags = PosSizeFlags::Pos; + // align left + { + if ( mbEmptyViewMargin ) + { + nViewOffX = 0; + nViewOffY = 0; + nViewHeight = nDlgHeight; + } + else + { + nViewOffX = WIZARDDIALOG_VIEW_DLGOFFSET_X; + nViewOffY = WIZARDDIALOG_VIEW_DLGOFFSET_Y; + nViewHeight = nDlgHeight-(WIZARDDIALOG_VIEW_DLGOFFSET_Y*2); + } + nViewPosFlags |= PosSizeFlags::Height; + } + mpViewWindow->setPosSizePixel( nViewOffX, nViewOffY, + nViewWidth, nViewHeight, + nViewPosFlags ); + } + + tools::Long RoadmapWizard::LogicalCoordinateToPixel(int iCoordinate) const + { + Size aLocSize = LogicToPixel(Size(iCoordinate, 0), MapMode(MapUnit::MapAppFont)); + int iPixelCoordinate = aLocSize.Width(); + return iPixelCoordinate; + } + + void RoadmapWizard::ImplPosTabPage() + { + if ( !mpCurTabPage ) + return; + + if ( !IsInInitShow() ) + { + // #100199# - On Unix initial size is equal to screen size, on Windows + // it's 0,0. One cannot calculate the size unless dialog is visible. + if ( !IsReallyVisible() ) + return; + } + + // calculate height of ButtonBar + tools::Long nMaxHeight = 0; + ImplWizButtonData* pBtnData = mpFirstBtn; + while ( pBtnData ) + { + tools::Long nBtnHeight = pBtnData->mpButton->GetSizePixel().Height(); + if ( nBtnHeight > nMaxHeight ) + nMaxHeight = nBtnHeight; + pBtnData = pBtnData->mpNext; + } + if ( nMaxHeight ) + nMaxHeight += WIZARDDIALOG_BUTTON_OFFSET_Y*2; + + // position TabPage + Size aDlgSize = GetOutputSizePixel(); + aDlgSize.AdjustHeight( -nMaxHeight ); + tools::Long nOffX = 0; + tools::Long nOffY = 0; + if ( mpViewWindow && mpViewWindow->IsVisible() ) + { + Size aViewSize = mpViewWindow->GetSizePixel(); + // align left + tools::Long nViewOffset = mbEmptyViewMargin ? 0 : WIZARDDIALOG_VIEW_DLGOFFSET_X; + nOffX += aViewSize.Width() + nViewOffset; + aDlgSize.AdjustWidth( -nOffX ); + } + Point aPos( nOffX, nOffY ); + mpCurTabPage->SetPosSizePixel( aPos, aDlgSize ); + } + + void RoadmapWizard::ImplShowTabPage( TabPage* pTabPage ) + { + if ( mpCurTabPage == pTabPage ) + return; + + TabPage* pOldTabPage = mpCurTabPage; + + mpCurTabPage = pTabPage; + if ( pTabPage ) + { + ImplPosTabPage(); + pTabPage->Show(); + } + + if ( pOldTabPage ) + pOldTabPage->Hide(); + } + + TabPage* RoadmapWizard::ImplGetPage( sal_uInt16 nLevel ) const + { + sal_uInt16 nTempLevel = 0; + ImplWizPageData* pPageData = mpFirstPage; + while ( pPageData ) + { + if ( (nTempLevel == nLevel) || !pPageData->mpNext ) + break; + + nTempLevel++; + pPageData = pPageData->mpNext; + } + + if ( pPageData ) + return pPageData->mpPage; + return nullptr; + } + + void RoadmapWizard::implConstruct( const WizardButtonFlags _nButtonFlags ) + { + m_xWizardImpl->sTitleBase = GetText(); + + // create the buttons according to the wizard button flags + // the help button + if (_nButtonFlags & WizardButtonFlags::HELP) + { + m_pHelp= VclPtr<HelpButton>::Create(this, WB_TABSTOP); + m_pHelp->SetSizePixel(LogicToPixel(Size(50, 14), MapMode(MapUnit::MapAppFont))); + m_pHelp->Show(); + AddButton( m_pHelp, WIZARDDIALOG_BUTTON_STDOFFSET_X); + } + + // the previous button + if (_nButtonFlags & WizardButtonFlags::PREVIOUS) + { + m_pPrevPage = VclPtr<PushButton>::Create(this, WB_TABSTOP); + m_pPrevPage->SetHelpId( HID_WIZARD_PREVIOUS ); + m_pPrevPage->SetSizePixel(LogicToPixel(Size(50, 14), MapMode(MapUnit::MapAppFont))); + m_pPrevPage->SetText(VclResId(STR_WIZDLG_PREVIOUS)); + m_pPrevPage->Show(); + m_pPrevPage->set_id("previous"); + + if (_nButtonFlags & WizardButtonFlags::NEXT) + AddButton( m_pPrevPage, ( WIZARDDIALOG_BUTTON_SMALLSTDOFFSET_X) ); // half x-offset to the next button + else + AddButton( m_pPrevPage, WIZARDDIALOG_BUTTON_STDOFFSET_X ); + mpPrevBtn = m_pPrevPage; + m_pPrevPage->SetClickHdl( LINK( this, RoadmapWizard, OnPrevPage ) ); + } + + // the next button + if (_nButtonFlags & WizardButtonFlags::NEXT) + { + m_pNextPage = VclPtr<PushButton>::Create(this, WB_TABSTOP); + m_pNextPage->SetHelpId( HID_WIZARD_NEXT ); + m_pNextPage->SetSizePixel(LogicToPixel(Size(50, 14), MapMode(MapUnit::MapAppFont))); + m_pNextPage->SetText(VclResId(STR_WIZDLG_NEXT)); + m_pNextPage->Show(); + m_pNextPage->set_id("next"); + + AddButton( m_pNextPage, WIZARDDIALOG_BUTTON_STDOFFSET_X ); + mpNextBtn = m_pNextPage; + m_pNextPage->SetClickHdl( LINK( this, RoadmapWizard, OnNextPage ) ); + } + + // the finish button + if (_nButtonFlags & WizardButtonFlags::FINISH) + { + m_pFinish = VclPtr<OKButton>::Create(this, WB_TABSTOP); + m_pFinish->SetSizePixel(LogicToPixel(Size(50, 14), MapMode(MapUnit::MapAppFont))); + m_pFinish->SetText(VclResId(STR_WIZDLG_FINISH)); + m_pFinish->Show(); + m_pFinish->set_id("finish"); + + AddButton( m_pFinish, WIZARDDIALOG_BUTTON_STDOFFSET_X ); + m_pFinish->SetClickHdl( LINK( this, RoadmapWizard, OnFinish ) ); + } + + // the cancel button + if (_nButtonFlags & WizardButtonFlags::CANCEL) + { + m_pCancel = VclPtr<CancelButton>::Create(this, WB_TABSTOP); + m_pCancel->SetSizePixel(LogicToPixel(Size(50, 14), MapMode(MapUnit::MapAppFont))); + m_pCancel->Show(); + + AddButton( m_pCancel, WIZARDDIALOG_BUTTON_STDOFFSET_X ); + } + } + + void RoadmapWizard::Resize() + { + if ( IsReallyShown() && !IsInInitShow() ) + { + ImplPosCtrls(); + ImplPosTabPage(); + } + + Dialog::Resize(); + } + + void RoadmapWizard::CalcAndSetSize() + { + Size aDlgSize = GetPageSizePixel(); + if ( !aDlgSize.Width() || !aDlgSize.Height() ) + { + ImplWizPageData* pPageData = mpFirstPage; + while ( pPageData ) + { + if ( pPageData->mpPage ) + { + Size aPageSize = pPageData->mpPage->GetSizePixel(); + if ( aPageSize.Width() > aDlgSize.Width() ) + aDlgSize.setWidth( aPageSize.Width() ); + if ( aPageSize.Height() > aDlgSize.Height() ) + aDlgSize.setHeight( aPageSize.Height() ); + } + + pPageData = pPageData->mpNext; + } + } + ImplCalcSize( aDlgSize ); + SetMinOutputSizePixel( aDlgSize ); + SetOutputSizePixel( aDlgSize ); + } + + void RoadmapWizard::StateChanged( StateChangedType nType ) + { + if ( nType == StateChangedType::InitShow ) + { + if ( IsDefaultSize() ) + { + CalcAndSetSize(); + } + + ImplPosCtrls(); + ImplPosTabPage(); + ImplShowTabPage( ImplGetPage( mnCurLevel ) ); + } + + Dialog::StateChanged( nType ); + } + + bool RoadmapWizard::EventNotify( NotifyEvent& rNEvt ) + { + if ( (rNEvt.GetType() == MouseNotifyEvent::KEYINPUT) && mpPrevBtn && mpNextBtn ) + { + const KeyEvent* pKEvt = rNEvt.GetKeyEvent(); + vcl::KeyCode aKeyCode = pKEvt->GetKeyCode(); + sal_uInt16 nKeyCode = aKeyCode.GetCode(); + + if ( aKeyCode.IsMod1() ) + { + if ( aKeyCode.IsShift() || (nKeyCode == KEY_PAGEUP) ) + { + if ( (nKeyCode == KEY_TAB) || (nKeyCode == KEY_PAGEUP) ) + { + if ( mpPrevBtn->IsVisible() && + mpPrevBtn->IsEnabled() && mpPrevBtn->IsInputEnabled() ) + { + mpPrevBtn->SetPressed( true ); + mpPrevBtn->SetPressed( false ); + mpPrevBtn->Click(); + } + return true; + } + } + else + { + if ( (nKeyCode == KEY_TAB) || (nKeyCode == KEY_PAGEDOWN) ) + { + if ( mpNextBtn->IsVisible() && + mpNextBtn->IsEnabled() && mpNextBtn->IsInputEnabled() ) + { + mpNextBtn->SetPressed( true ); + mpNextBtn->SetPressed( false ); + mpNextBtn->Click(); + } + return true; + } + } + } + } + + return Dialog::EventNotify( rNEvt ); + } + + void RoadmapWizard::GetOrCreatePage( const WizardTypes::WizardState i_nState ) + { + if ( nullptr != GetPage( i_nState ) ) + return; + + VclPtr<TabPage> pNewPage = createPage( i_nState ); + DBG_ASSERT( pNewPage, "RoadmapWizard::GetOrCreatePage: invalid new page (NULL)!" ); + + // fill up the page sequence of our base class (with dummies) + while ( m_xWizardImpl->nFirstUnknownPage < i_nState ) + { + AddPage( nullptr ); + ++m_xWizardImpl->nFirstUnknownPage; + } + + if ( m_xWizardImpl->nFirstUnknownPage == i_nState ) + { + // encountered this page number the first time + AddPage( pNewPage ); + ++m_xWizardImpl->nFirstUnknownPage; + } + else + // already had this page - just change it + SetPage( i_nState, pNewPage ); + } + + void RoadmapWizard::ActivatePage() + { + WizardTypes::WizardState nCurrentLevel = GetCurLevel(); + GetOrCreatePage( nCurrentLevel ); + + enterState( nCurrentLevel ); + } + + bool RoadmapWizard::ShowPage( sal_uInt16 nLevel ) + { + mnCurLevel = nLevel; + ActivatePage(); + ImplShowTabPage( ImplGetPage( mnCurLevel ) ); + return true; + } + + void RoadmapWizard::Finish( tools::Long nResult ) + { + if ( IsInExecute() ) + EndDialog( nResult ); + else if ( GetStyle() & WB_CLOSEABLE ) + Close(); + } + + void RoadmapWizard::AddPage( TabPage* pPage ) + { + ImplWizPageData* pNewPageData = new ImplWizPageData; + pNewPageData->mpNext = nullptr; + pNewPageData->mpPage = pPage; + + if ( !mpFirstPage ) + mpFirstPage = pNewPageData; + else + { + ImplWizPageData* pPageData = mpFirstPage; + while ( pPageData->mpNext ) + pPageData = pPageData->mpNext; + pPageData->mpNext = pNewPageData; + } + } + + void RoadmapWizard::RemovePage( TabPage* pPage ) + { + ImplWizPageData* pPrevPageData = nullptr; + ImplWizPageData* pPageData = mpFirstPage; + while ( pPageData ) + { + if ( pPageData->mpPage == pPage ) + { + if ( pPrevPageData ) + pPrevPageData->mpNext = pPageData->mpNext; + else + mpFirstPage = pPageData->mpNext; + if ( pPage == mpCurTabPage ) + mpCurTabPage = nullptr; + delete pPageData; + return; + } + + pPrevPageData = pPageData; + pPageData = pPageData->mpNext; + } + + OSL_FAIL( "RoadmapWizard::RemovePage() - Page not in list" ); + } + + void RoadmapWizard::SetPage( sal_uInt16 nLevel, TabPage* pPage ) + { + sal_uInt16 nTempLevel = 0; + ImplWizPageData* pPageData = mpFirstPage; + while ( pPageData ) + { + if ( (nTempLevel == nLevel) || !pPageData->mpNext ) + break; + + nTempLevel++; + pPageData = pPageData->mpNext; + } + + if ( pPageData ) + { + if ( pPageData->mpPage == mpCurTabPage ) + mpCurTabPage = nullptr; + pPageData->mpPage = pPage; + } + } + + TabPage* RoadmapWizard::GetPage( sal_uInt16 nLevel ) const + { + sal_uInt16 nTempLevel = 0; + + for (ImplWizPageData* pPageData = mpFirstPage; pPageData; + pPageData = pPageData->mpNext) + { + if ( nTempLevel == nLevel ) + return pPageData->mpPage; + nTempLevel++; + } + + return nullptr; + } + + void RoadmapWizard::AddButton( Button* pButton, tools::Long nOffset ) + { + ImplWizButtonData* pNewBtnData = new ImplWizButtonData; + pNewBtnData->mpNext = nullptr; + pNewBtnData->mpButton = pButton; + pNewBtnData->mnOffset = nOffset; + + if ( !mpFirstBtn ) + mpFirstBtn = pNewBtnData; + else + { + ImplWizButtonData* pBtnData = mpFirstBtn; + while ( pBtnData->mpNext ) + pBtnData = pBtnData->mpNext; + pBtnData->mpNext = pNewBtnData; + } + } + + void RoadmapWizard::RemoveButton( Button* pButton ) + { + ImplWizButtonData* pPrevBtnData = nullptr; + ImplWizButtonData* pBtnData = mpFirstBtn; + while ( pBtnData ) + { + if ( pBtnData->mpButton == pButton ) + { + if ( pPrevBtnData ) + pPrevBtnData->mpNext = pBtnData->mpNext; + else + mpFirstBtn = pBtnData->mpNext; + delete pBtnData; + return; + } + + pPrevBtnData = pBtnData; + pBtnData = pBtnData->mpNext; + } + + OSL_FAIL( "RoadmapWizard::RemoveButton() - Button not in list" ); + } + + IMPL_LINK_NOARG(RoadmapWizard, OnFinish, Button*, void) + { + if ( isTravelingSuspended() ) + return; + RoadmapWizardTravelSuspension aTravelGuard( *this ); + Finish( RET_OK ); + } + + bool RoadmapWizard::skipBackwardUntil( WizardTypes::WizardState _nTargetState ) + { + // don't travel directly on m_xWizardImpl->aStateHistory, in case something goes wrong + std::stack< WizardTypes::WizardState > aTravelVirtually = m_xWizardImpl->aStateHistory; + std::stack< WizardTypes::WizardState > aOldStateHistory = m_xWizardImpl->aStateHistory; + + WizardTypes::WizardState nCurrentRollbackState = getCurrentState(); + while ( nCurrentRollbackState != _nTargetState ) + { + DBG_ASSERT( !aTravelVirtually.empty(), "RoadmapWizard::skipBackwardUntil: this target state does not exist in the history!" ); + nCurrentRollbackState = aTravelVirtually.top(); + aTravelVirtually.pop(); + } + m_xWizardImpl->aStateHistory = aTravelVirtually; + if ( !ShowPage( _nTargetState ) ) + { + m_xWizardImpl->aStateHistory = aOldStateHistory; + return false; + } + return true; + } + + bool RoadmapWizard::skipUntil( WizardTypes::WizardState _nTargetState ) + { + WizardTypes::WizardState nCurrentState = getCurrentState(); + + // don't travel directly on m_xWizardImpl->aStateHistory, in case something goes wrong + std::stack< WizardTypes::WizardState > aTravelVirtually = m_xWizardImpl->aStateHistory; + std::stack< WizardTypes::WizardState > aOldStateHistory = m_xWizardImpl->aStateHistory; + while ( nCurrentState != _nTargetState ) + { + WizardTypes::WizardState nNextState = determineNextState( nCurrentState ); + if ( WZS_INVALID_STATE == nNextState ) + { + OSL_FAIL( "RoadmapWizard::skipUntil: the given target state does not exist!" ); + return false; + } + + // remember the skipped state in the history + aTravelVirtually.push( nCurrentState ); + + // get the next state + nCurrentState = nNextState; + } + m_xWizardImpl->aStateHistory = aTravelVirtually; + // show the target page + if ( !ShowPage( nCurrentState ) ) + { + // argh! prepareLeaveCurrentPage succeeded, determineNextState succeeded, + // but ShowPage doesn't? Somebody behaves very strange here... + OSL_FAIL( "RoadmapWizard::skipUntil: very unpolite..." ); + m_xWizardImpl->aStateHistory = aOldStateHistory; + return false; + } + return true; + } + + void RoadmapWizard::travelNext() + { + // determine the next state to travel to + WizardTypes::WizardState nCurrentState = getCurrentState(); + WizardTypes::WizardState nNextState = determineNextState(nCurrentState); + if (WZS_INVALID_STATE == nNextState) + return; + + // the state history is used by the enterState method + // all fine + m_xWizardImpl->aStateHistory.push(nCurrentState); + if (!ShowPage(nNextState)) + { + m_xWizardImpl->aStateHistory.pop(); + } + } + + void RoadmapWizard::travelPrevious() + { + DBG_ASSERT(!m_xWizardImpl->aStateHistory.empty(), "RoadmapWizard::travelPrevious: have no previous page!"); + + // the next state to switch to + WizardTypes::WizardState nPreviousState = m_xWizardImpl->aStateHistory.top(); + + // the state history is used by the enterState method + m_xWizardImpl->aStateHistory.pop(); + // show this page + if (!ShowPage(nPreviousState)) + { + m_xWizardImpl->aStateHistory.push(nPreviousState); + } + + // all fine + } + + void RoadmapWizard::removePageFromHistory( WizardTypes::WizardState nToRemove ) + { + + std::stack< WizardTypes::WizardState > aTemp; + while(!m_xWizardImpl->aStateHistory.empty()) + { + WizardTypes::WizardState nPreviousState = m_xWizardImpl->aStateHistory.top(); + m_xWizardImpl->aStateHistory.pop(); + if(nPreviousState != nToRemove) + aTemp.push( nPreviousState ); + else + break; + } + while(!aTemp.empty()) + { + m_xWizardImpl->aStateHistory.push( aTemp.top() ); + aTemp.pop(); + } + } + + IMPL_LINK_NOARG(RoadmapWizard, OnPrevPage, Button*, void) + { + if ( isTravelingSuspended() ) + return; + RoadmapWizardTravelSuspension aTravelGuard( *this ); + travelPrevious(); + } + + IMPL_LINK_NOARG(RoadmapWizard, OnNextPage, Button*, void) + { + if ( isTravelingSuspended() ) + return; + RoadmapWizardTravelSuspension aTravelGuard( *this ); + travelNext(); + } + + bool RoadmapWizard::isTravelingSuspended() const + { + return m_xWizardImpl->m_bTravelingSuspended; + } + + void RoadmapWizard::suspendTraveling( AccessGuard ) + { + DBG_ASSERT( !m_xWizardImpl->m_bTravelingSuspended, "RoadmapWizard::suspendTraveling: already suspended!" ); + m_xWizardImpl->m_bTravelingSuspended = true; + } + + void RoadmapWizard::resumeTraveling( AccessGuard ) + { + DBG_ASSERT( m_xWizardImpl->m_bTravelingSuspended, "RoadmapWizard::resumeTraveling: nothing to resume!" ); + m_xWizardImpl->m_bTravelingSuspended = false; + } + + WizardMachine::WizardMachine(weld::Window* pParent, WizardButtonFlags nButtonFlags) + : AssistantController(pParent, "vcl/ui/wizard.ui", "Wizard") + , m_pCurTabPage(nullptr) + , m_nCurState(0) + , m_pFirstPage(nullptr) + , m_xFinish(m_xAssistant->weld_widget_for_response(RET_OK)) + , m_xCancel(m_xAssistant->weld_widget_for_response(RET_CANCEL)) + , m_xNextPage(m_xAssistant->weld_widget_for_response(RET_YES)) + , m_xPrevPage(m_xAssistant->weld_widget_for_response(RET_NO)) + , m_xHelp(m_xAssistant->weld_widget_for_response(RET_HELP)) + , m_pImpl(new WizardMachineImplData) + { + implConstruct(nButtonFlags); + } + + void WizardMachine::implConstruct(const WizardButtonFlags nButtonFlags) + { + m_pImpl->sTitleBase = m_xAssistant->get_title(); + + const bool bHideHelp = comphelper::LibreOfficeKit::isActive() && + officecfg::Office::Common::Help::HelpRootURL::get().isEmpty(); + // create the buttons according to the wizard button flags + // the help button + if (nButtonFlags & WizardButtonFlags::HELP && !bHideHelp) + m_xHelp->show(); + else + m_xHelp->hide(); + + // the previous button + if (nButtonFlags & WizardButtonFlags::PREVIOUS) + { + m_xPrevPage->set_help_id( HID_WIZARD_PREVIOUS ); + m_xPrevPage->show(); + + m_xPrevPage->connect_clicked( LINK( this, WizardMachine, OnPrevPage ) ); + } + else + m_xPrevPage->hide(); + + // the next button + if (nButtonFlags & WizardButtonFlags::NEXT) + { + m_xNextPage->set_help_id( HID_WIZARD_NEXT ); + m_xNextPage->show(); + + m_xNextPage->connect_clicked( LINK( this, WizardMachine, OnNextPage ) ); + } + else + m_xNextPage->hide(); + + // the finish button + if (nButtonFlags & WizardButtonFlags::FINISH) + { + m_xFinish->show(); + + m_xFinish->connect_clicked( LINK( this, WizardMachine, OnFinish ) ); + } + else + m_xFinish->hide(); + + // the cancel button + if (nButtonFlags & WizardButtonFlags::CANCEL) + { + m_xCancel->show(); + m_xCancel->connect_clicked( LINK( this, WizardMachine, OnCancel ) ); + } + else + m_xCancel->hide(); + } + + WizardMachine::~WizardMachine() + { + if (m_pImpl) + { + while (m_pFirstPage) + RemovePage(m_pFirstPage->mxPage.get()); + m_pImpl.reset(); + } + } + + void WizardMachine::implUpdateTitle() + { + OUString sCompleteTitle(m_pImpl->sTitleBase); + + // append the page title + BuilderPage* pCurrentPage = GetPage(getCurrentState()); + if ( pCurrentPage && !pCurrentPage->GetPageTitle().isEmpty() ) + { + sCompleteTitle += " - " + pCurrentPage->GetPageTitle(); + } + + m_xAssistant->set_title(sCompleteTitle); + } + + void WizardMachine::setTitleBase(const OUString& _rTitleBase) + { + m_pImpl->sTitleBase = _rTitleBase; + implUpdateTitle(); + } + + BuilderPage* WizardMachine::GetOrCreatePage( const WizardTypes::WizardState i_nState ) + { + if ( nullptr == GetPage( i_nState ) ) + { + std::unique_ptr<BuilderPage> xNewPage = createPage( i_nState ); + DBG_ASSERT( xNewPage, "WizardMachine::GetOrCreatePage: invalid new page (NULL)!" ); + + // fill up the page sequence of our base class (with dummies) + while ( m_pImpl->nFirstUnknownPage < i_nState ) + { + AddPage( nullptr ); + ++m_pImpl->nFirstUnknownPage; + } + + if ( m_pImpl->nFirstUnknownPage == i_nState ) + { + // encountered this page number the first time + AddPage(std::move(xNewPage)); + ++m_pImpl->nFirstUnknownPage; + } + else + // already had this page - just change it + SetPage(i_nState, std::move(xNewPage)); + } + return GetPage( i_nState ); + } + + void WizardMachine::ActivatePage() + { + WizardTypes::WizardState nCurrentLevel = m_nCurState; + GetOrCreatePage( nCurrentLevel ); + + enterState( nCurrentLevel ); + } + + bool WizardMachine::DeactivatePage() + { + WizardTypes::WizardState nCurrentState = getCurrentState(); + return leaveState(nCurrentState); + } + + void WizardMachine::defaultButton(WizardButtonFlags _nWizardButtonFlags) + { + // the new default button + weld::Button* pNewDefButton = nullptr; + if (_nWizardButtonFlags & WizardButtonFlags::FINISH) + pNewDefButton = m_xFinish.get(); + if (_nWizardButtonFlags & WizardButtonFlags::NEXT) + pNewDefButton = m_xNextPage.get(); + if (_nWizardButtonFlags & WizardButtonFlags::PREVIOUS) + pNewDefButton = m_xPrevPage.get(); + if (_nWizardButtonFlags & WizardButtonFlags::HELP) + pNewDefButton = m_xHelp.get(); + if (_nWizardButtonFlags & WizardButtonFlags::CANCEL) + pNewDefButton = m_xCancel.get(); + + defaultButton(pNewDefButton); + } + + void WizardMachine::defaultButton(weld::Button* _pNewDefButton) + { + // loop through all (direct and indirect) descendants which participate in our tabbing order, and + // reset the WB_DEFBUTTON for every window which is a button and set _pNewDefButton as the new + // WB_DEFBUTTON + m_xAssistant->change_default_widget(nullptr, _pNewDefButton); + } + + void WizardMachine::enableButtons(WizardButtonFlags _nWizardButtonFlags, bool _bEnable) + { + if (_nWizardButtonFlags & WizardButtonFlags::FINISH) + m_xFinish->set_sensitive(_bEnable); + if (_nWizardButtonFlags & WizardButtonFlags::NEXT) + m_xNextPage->set_sensitive(_bEnable); + if (_nWizardButtonFlags & WizardButtonFlags::PREVIOUS) + m_xPrevPage->set_sensitive(_bEnable); + if (_nWizardButtonFlags & WizardButtonFlags::HELP) + m_xHelp->set_sensitive(_bEnable); + if (_nWizardButtonFlags & WizardButtonFlags::CANCEL) + m_xCancel->set_sensitive(_bEnable); + } + + void WizardMachine::enterState(WizardTypes::WizardState _nState) + { + // tell the page + IWizardPageController* pController = getPageController( GetPage( _nState ) ); + OSL_ENSURE( pController, "WizardMachine::enterState: no controller for the given page!" ); + if ( pController ) + pController->initializePage(); + + if ( isAutomaticNextButtonStateEnabled() ) + enableButtons( WizardButtonFlags::NEXT, canAdvance() ); + + enableButtons( WizardButtonFlags::PREVIOUS, !m_pImpl->aStateHistory.empty() ); + + // set the new title - it depends on the current page (i.e. state) + implUpdateTitle(); + } + + bool WizardMachine::leaveState(WizardTypes::WizardState) + { + // no need to ask the page here. + // If we reach this point, we already gave the current page the chance to commit it's data, + // and it was allowed to commit it's data + + return true; + } + + bool WizardMachine::onFinish() + { + return Finish(RET_OK); + } + + IMPL_LINK_NOARG(WizardMachine, OnFinish, weld::Button&, void) + { + if ( isTravelingSuspended() ) + return; + + // prevent WizardTravelSuspension from using this instance + // after will be destructed due to onFinish and async response call + { + WizardTravelSuspension aTravelGuard( *this ); + if (!prepareLeaveCurrentState(WizardTypes::eFinish)) + { + return; + } + } + + onFinish(); + } + + IMPL_LINK_NOARG(WizardMachine, OnCancel, weld::Button&, void) + { + m_xAssistant->response(RET_CANCEL); + } + + WizardTypes::WizardState WizardMachine::determineNextState(WizardTypes::WizardState _nCurrentState ) const + { + return _nCurrentState + 1; + } + + bool WizardMachine::prepareLeaveCurrentState( WizardTypes::CommitPageReason _eReason ) + { + IWizardPageController* pController = getPageController( GetPage( getCurrentState() ) ); + ENSURE_OR_RETURN( pController != nullptr, "WizardMachine::prepareLeaveCurrentState: no controller for the current page!", true ); + return pController->commitPage( _eReason ); + } + + bool WizardMachine::skipBackwardUntil(WizardTypes::WizardState _nTargetState) + { + // allowed to leave the current page? + if ( !prepareLeaveCurrentState( WizardTypes::eTravelBackward ) ) + return false; + + // don't travel directly on m_pImpl->aStateHistory, in case something goes wrong + std::stack< WizardTypes::WizardState > aTravelVirtually = m_pImpl->aStateHistory; + std::stack< WizardTypes::WizardState > aOldStateHistory = m_pImpl->aStateHistory; + + WizardTypes::WizardState nCurrentRollbackState = getCurrentState(); + while ( nCurrentRollbackState != _nTargetState ) + { + DBG_ASSERT( !aTravelVirtually.empty(), "WizardMachine::skipBackwardUntil: this target state does not exist in the history!" ); + nCurrentRollbackState = aTravelVirtually.top(); + aTravelVirtually.pop(); + } + m_pImpl->aStateHistory = aTravelVirtually; + if ( !ShowPage( _nTargetState ) ) + { + m_pImpl->aStateHistory = aOldStateHistory; + return false; + } + return true; + } + + bool WizardMachine::skipUntil( WizardTypes::WizardState _nTargetState ) + { + WizardTypes::WizardState nCurrentState = getCurrentState(); + + // allowed to leave the current page? + if ( !prepareLeaveCurrentState( nCurrentState < _nTargetState ? WizardTypes::eTravelForward : WizardTypes::eTravelBackward ) ) + return false; + + // don't travel directly on m_pImpl->aStateHistory, in case something goes wrong + std::stack< WizardTypes::WizardState > aTravelVirtually = m_pImpl->aStateHistory; + std::stack< WizardTypes::WizardState > aOldStateHistory = m_pImpl->aStateHistory; + while ( nCurrentState != _nTargetState ) + { + WizardTypes::WizardState nNextState = determineNextState( nCurrentState ); + if ( WZS_INVALID_STATE == nNextState ) + { + OSL_FAIL( "WizardMachine::skipUntil: the given target state does not exist!" ); + return false; + } + + // remember the skipped state in the history + aTravelVirtually.push( nCurrentState ); + + // get the next state + nCurrentState = nNextState; + } + m_pImpl->aStateHistory = aTravelVirtually; + // show the target page + if ( !ShowPage( nCurrentState ) ) + { + // argh! prepareLeaveCurrentPage succeeded, determineNextState succeeded, + // but ShowPage doesn't? Somebody behaves very strange here... + OSL_FAIL( "WizardMachine::skipUntil: very unpolite..." ); + m_pImpl->aStateHistory = aOldStateHistory; + return false; + } + return true; + } + + void WizardMachine::skip() + { + // allowed to leave the current page? + if ( !prepareLeaveCurrentState( WizardTypes::eTravelForward ) ) + return; + + WizardTypes::WizardState nCurrentState = getCurrentState(); + WizardTypes::WizardState nNextState = determineNextState(nCurrentState); + + if (WZS_INVALID_STATE == nNextState) + return; + + // remember the skipped state in the history + m_pImpl->aStateHistory.push(nCurrentState); + + // get the next state + nCurrentState = nNextState; + + // show the (n+1)th page + if (!ShowPage(nCurrentState)) + { + // TODO: this leaves us in a state where we have no current page and an inconsistent state history. + // Perhaps we should rollback the skipping here... + OSL_FAIL("RoadmapWizard::skip: very unpolite..."); + // if somebody does a skip and then does not allow to leave... + // (can't be a commit error, as we've already committed the current page. So if ShowPage fails here, + // somebody behaves really strange ...) + return; + } + + // all fine + } + + bool WizardMachine::travelNext() + { + // allowed to leave the current page? + if ( !prepareLeaveCurrentState( WizardTypes::eTravelForward ) ) + return false; + + // determine the next state to travel to + WizardTypes::WizardState nCurrentState = getCurrentState(); + WizardTypes::WizardState nNextState = determineNextState(nCurrentState); + if (WZS_INVALID_STATE == nNextState) + return false; + + // the state history is used by the enterState method + // all fine + m_pImpl->aStateHistory.push(nCurrentState); + if (!ShowPage(nNextState)) + { + m_pImpl->aStateHistory.pop(); + return false; + } + + return true; + } + + bool WizardMachine::ShowPage(WizardTypes::WizardState nState) + { + if (DeactivatePage()) + { + BuilderPage* pOldTabPage = m_pCurTabPage; + + m_nCurState = nState; + ActivatePage(); + + if (pOldTabPage) + pOldTabPage->Deactivate(); + + m_xAssistant->set_current_page(OString::number(nState)); + + m_pCurTabPage = GetPage(m_nCurState); + m_pCurTabPage->Activate(); + + return true; + } + return false; + } + + bool WizardMachine::ShowNextPage() + { + return ShowPage(m_nCurState + 1); + } + + bool WizardMachine::ShowPrevPage() + { + if (!m_nCurState) + return false; + return ShowPage(m_nCurState - 1); + } + + bool WizardMachine::travelPrevious() + { + DBG_ASSERT(!m_pImpl->aStateHistory.empty(), "WizardMachine::travelPrevious: have no previous page!"); + + // allowed to leave the current page? + if ( !prepareLeaveCurrentState( WizardTypes::eTravelBackward ) ) + return false; + + // the next state to switch to + WizardTypes::WizardState nPreviousState = m_pImpl->aStateHistory.top(); + + // the state history is used by the enterState method + m_pImpl->aStateHistory.pop(); + // show this page + if (!ShowPage(nPreviousState)) + { + m_pImpl->aStateHistory.push(nPreviousState); + return false; + } + + // all fine + return true; + } + + + void WizardMachine::removePageFromHistory( WizardTypes::WizardState nToRemove ) + { + + std::stack< WizardTypes::WizardState > aTemp; + while(!m_pImpl->aStateHistory.empty()) + { + WizardTypes::WizardState nPreviousState = m_pImpl->aStateHistory.top(); + m_pImpl->aStateHistory.pop(); + if(nPreviousState != nToRemove) + aTemp.push( nPreviousState ); + else + break; + } + while(!aTemp.empty()) + { + m_pImpl->aStateHistory.push( aTemp.top() ); + aTemp.pop(); + } + } + + + void WizardMachine::enableAutomaticNextButtonState() + { + m_pImpl->m_bAutoNextButtonState = true; + } + + + bool WizardMachine::isAutomaticNextButtonStateEnabled() const + { + return m_pImpl->m_bAutoNextButtonState; + } + + IMPL_LINK_NOARG(WizardMachine, OnPrevPage, weld::Button&, void) + { + if ( isTravelingSuspended() ) + return; + WizardTravelSuspension aTravelGuard( *this ); + travelPrevious(); + } + + IMPL_LINK_NOARG(WizardMachine, OnNextPage, weld::Button&, void) + { + if ( isTravelingSuspended() ) + return; + WizardTravelSuspension aTravelGuard( *this ); + travelNext(); + } + + IWizardPageController* WizardMachine::getPageController(BuilderPage* pCurrentPage) const + { + IWizardPageController* pController = dynamic_cast<IWizardPageController*>(pCurrentPage); + return pController; + } + + void WizardMachine::getStateHistory( std::vector< WizardTypes::WizardState >& _out_rHistory ) + { + std::stack< WizardTypes::WizardState > aHistoryCopy( m_pImpl->aStateHistory ); + while ( !aHistoryCopy.empty() ) + { + _out_rHistory.push_back( aHistoryCopy.top() ); + aHistoryCopy.pop(); + } + } + + bool WizardMachine::canAdvance() const + { + return WZS_INVALID_STATE != determineNextState( getCurrentState() ); + } + + void WizardMachine::updateTravelUI() + { + const IWizardPageController* pController = getPageController( GetPage( getCurrentState() ) ); + OSL_ENSURE( pController != nullptr, "RoadmapWizard::updateTravelUI: no controller for the current page!" ); + + bool bCanAdvance = + ( !pController || pController->canAdvance() ) // the current page allows to advance + && canAdvance(); // the dialog as a whole allows to advance + enableButtons( WizardButtonFlags::NEXT, bCanAdvance ); + } + + bool WizardMachine::isTravelingSuspended() const + { + return m_pImpl->m_bTravelingSuspended; + } + + void WizardMachine::suspendTraveling( AccessGuard ) + { + DBG_ASSERT( !m_pImpl->m_bTravelingSuspended, "WizardMachine::suspendTraveling: already suspended!" ); + m_pImpl->m_bTravelingSuspended = true; + } + + void WizardMachine::resumeTraveling( AccessGuard ) + { + if (!m_pImpl) + return; + + DBG_ASSERT( m_pImpl->m_bTravelingSuspended, "WizardMachine::resumeTraveling: nothing to resume!" ); + m_pImpl->m_bTravelingSuspended = false; + } + + bool WizardMachine::Finish(short nResult) + { + if ( DeactivatePage() ) + { + if (m_pCurTabPage) + m_pCurTabPage->Deactivate(); + + m_xAssistant->response(nResult); + return true; + } + else + return false; + } + + void WizardMachine::AddPage(std::unique_ptr<BuilderPage> xPage) + { + WizPageData* pNewPageData = new WizPageData; + pNewPageData->mpNext = nullptr; + pNewPageData->mxPage = std::move(xPage); + + if ( !m_pFirstPage ) + m_pFirstPage = pNewPageData; + else + { + WizPageData* pPageData = m_pFirstPage; + while ( pPageData->mpNext ) + pPageData = pPageData->mpNext; + pPageData->mpNext = pNewPageData; + } + } + + void WizardMachine::RemovePage(const BuilderPage* pPage) + { + WizPageData* pPrevPageData = nullptr; + WizPageData* pPageData = m_pFirstPage; + while ( pPageData ) + { + if (pPageData->mxPage.get() == pPage) + { + if (pPrevPageData) + pPrevPageData->mpNext = pPageData->mpNext; + else + m_pFirstPage = pPageData->mpNext; + if (pPage == m_pCurTabPage) + m_pCurTabPage = nullptr; + delete pPageData; + return; + } + + pPrevPageData = pPageData; + pPageData = pPageData->mpNext; + } + + OSL_FAIL( "WizardMachine::RemovePage() - Page not in list" ); + } + + void WizardMachine::SetPage(WizardTypes::WizardState nLevel, std::unique_ptr<BuilderPage> xPage) + { + sal_uInt16 nTempLevel = 0; + WizPageData* pPageData = m_pFirstPage; + while ( pPageData ) + { + if ( (nTempLevel == nLevel) || !pPageData->mpNext ) + break; + + nTempLevel++; + pPageData = pPageData->mpNext; + } + + if ( pPageData ) + { + if (pPageData->mxPage.get() == m_pCurTabPage) + m_pCurTabPage = nullptr; + pPageData->mxPage = std::move(xPage); + } + } + + BuilderPage* WizardMachine::GetPage(WizardTypes::WizardState nLevel) const + { + sal_uInt16 nTempLevel = 0; + + for (WizPageData* pPageData = m_pFirstPage; pPageData; + pPageData = pPageData->mpNext) + { + if ( nTempLevel == nLevel ) + return pPageData->mxPage.get(); + nTempLevel++; + } + + return nullptr; + } +} // namespace svt + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/control/wizimpldata.hxx b/vcl/source/control/wizimpldata.hxx new file mode 100644 index 000000000..f943c0473 --- /dev/null +++ b/vcl/source/control/wizimpldata.hxx @@ -0,0 +1,64 @@ +/* -*- 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 . + */ + +#ifndef INCLUDED_VCL_SOURCE_CONTROL_WIZIMPLDATA_HXX +#define INCLUDED_VCL_SOURCE_CONTROL_WIZIMPLDATA_HXX + +#include <stack> + +struct WizPageData +{ + WizPageData* mpNext; + std::unique_ptr<BuilderPage> mxPage; +}; + +struct ImplWizButtonData +{ + ImplWizButtonData* mpNext; + VclPtr<Button> mpButton; + tools::Long mnOffset; +}; + +namespace vcl +{ + struct WizardMachineImplData + { + OUString sTitleBase; // the base for the title + std::stack<WizardTypes::WizardState> aStateHistory; // the history of all states (used for implementing "Back") + + WizardTypes::WizardState nFirstUnknownPage; + // the WizardDialog does not allow non-linear transitions (e.g. it's + // not possible to add pages in a non-linear order), so we need some own maintenance data + + bool m_bAutoNextButtonState; + + bool m_bTravelingSuspended; + + WizardMachineImplData() + :nFirstUnknownPage( 0 ) + ,m_bAutoNextButtonState( false ) + ,m_bTravelingSuspended( false ) + { + } + }; +} // namespace svt + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |