cui/source/inc/paragrph.hxx | 1 cui/source/tabpages/chardlg.cxx | 2 cui/source/tabpages/paragrph.cxx | 8 cui/source/tabpages/tptrans.cxx | 2 include/sfx2/sfxdlg.hxx | 5 include/sfx2/strings.hrc | 2 include/sfx2/tabdlg.hxx | 24 +- include/svl/itemset.hxx | 5 include/svl/style.hxx | 8 sc/inc/stlsheet.hxx | 3 sc/inc/strings.hrc | 13 + sc/source/core/data/stlsheet.cxx | 189 ++++++++++++++++ sfx2/UIConfig_sfx.mk | 3 sfx2/source/dialog/mgetempl.cxx | 371 +++++++++++++++++++++++++++++++- sfx2/source/dialog/mgetempl.hxx | 84 ++++++- sfx2/source/dialog/styledlg.cxx | 7 sfx2/source/dialog/tabdlg.cxx | 237 ++++++++++++++++++++ sfx2/uiconfig/ui/managestylepage.ui | 117 +++++++++- sfx2/uiconfig/ui/propertycategoryrow.ui | 42 +++ sfx2/uiconfig/ui/propertychip.ui | 55 ++++ sfx2/uiconfig/ui/propertychiprow.ui | 10 solenv/sanitizers/ui/sfx.suppr | 2 svl/source/items/itemset.cxx | 18 + svl/source/items/style.cxx | 49 ++++ sw/inc/docstyle.hxx | 3 sw/inc/strings.hrc | 14 + sw/qa/uitest/ui/fmtui/tdf89826.py | 95 ++++++++ sw/source/ui/dialog/swdlgfact.cxx | 2 sw/source/ui/fmtui/tmpdlg.cxx | 2 sw/source/uibase/app/docst.cxx | 6 sw/source/uibase/app/docstyle.cxx | 270 +++++++++++++++++++++++ 31 files changed, 1627 insertions(+), 22 deletions(-)
New commits: commit 52010fde0917051e043674c8da42d9a06340b571 Author: Paolo Benvenuto <[email protected]> AuthorDate: Sat Feb 28 13:22:16 2026 +0100 Commit: Heiko Tietze <[email protected]> CommitDate: Sat Feb 28 15:52:40 2026 +0100 tdf#89826 Add interactive property chips to style Organizer tab The Organizer tab of the style dialog now shows interactive "chips" for each property that differs from the parent style. Each chip displays the property name and value, and includes an X button to reset that property back to parent inheritance. All properties are shown full text, no ellipsis. Properties are grouped by tab page (Font, Font effects, etc.) and displayed in a scrollable area. A View/Edit toggle lets the user switch between the traditional description text and the interactive chip view. Implementation summary: - New PropertyChip and PropertyCategoryRow widgets (sfx2) with corresponding .ui files for the chip layout. - SfxStyleSheetBase::GetItemPresentation() virtual method returns per-property presentation strings, overridden in SwDocStyleSheet and ScStyleSheet for module-specific attributes. - SfxItemSet access tracker mechanism to automatically map WhichIds to their owning tab pages via BuildWhichToTabMap(). - SfxTabDialogController::InvalidateItem() propagates chip resets to the output set and affected tab pages. - SwDocStyleSheet::ResetItems() uses SwDoc::ResetAttrAtFormat() to ensure proper undo support in Writer. - SfxStyleDialogController::Ok() clears invalidated items from the style's item set on dialog confirmation. - SvxAsianTabPage now calls SetExchangeSupport() and implements DeactivatePage() so its items are properly tracked. - Access tracker ignore guards in SvxCharBasePage::SetPrevFontWidthScale and SvxTransparenceTabPage::InitPreview to avoid false positives from preview-only item access. Includes a UITest for Writer (sw/qa/uitest/ui/fmtui/tdf89826.py) that creates a child style, modifies a property, removes it via chip, and verifies inheritance is restored including undo. A Calc UITest is not included because the style edit dialog in Calc is opened asynchronously (ScTabViewShell::ExecuteStyleEdit) and the UITest framework's execute_dialog_through_command cannot capture it. The Calc functionality has been verified manually. A Calc UITest can be added once the framework supports async style dialogs. Change-Id: Ia764405798c212fe5340bba254f2ff6ac62365f6 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199318 Reviewed-by: Heiko Tietze <[email protected]> Tested-by: Jenkins diff --git a/cui/source/inc/paragrph.hxx b/cui/source/inc/paragrph.hxx index 685492aa8e61..ad4398495f3f 100644 --- a/cui/source/inc/paragrph.hxx +++ b/cui/source/inc/paragrph.hxx @@ -357,6 +357,7 @@ public: virtual bool FillItemSet( SfxItemSet* rSet ) override; virtual void Reset( const SfxItemSet* rSet ) override; + virtual DeactivateRC DeactivatePage( SfxItemSet* pSet ) override; virtual void ChangesApplied() override; }; diff --git a/cui/source/tabpages/chardlg.cxx b/cui/source/tabpages/chardlg.cxx index a1a0130ac6c6..a92ae65f5ffa 100644 --- a/cui/source/tabpages/chardlg.cxx +++ b/cui/source/tabpages/chardlg.cxx @@ -154,11 +154,13 @@ void SvxCharBasePage::ActivatePage(const SfxItemSet& rSet) void SvxCharBasePage::SetPrevFontWidthScale( const SfxItemSet& rSet ) { sal_uInt16 nWhich = GetWhich( SID_ATTR_CHAR_SCALEWIDTH ); + rSet.SetAccessTrackerIgnore(true); if (rSet.GetItemState(nWhich)>=SfxItemState::DEFAULT) { const SvxCharScaleWidthItem &rItem = static_cast<const SvxCharScaleWidthItem&>( rSet.Get( nWhich ) ); m_aPreviewWin.SetFontWidthScale(rItem.GetValue()); } + rSet.SetAccessTrackerIgnore(false); } void SvxCharBasePage::SetPrevFontEscapement( sal_uInt8 nProp, sal_uInt8 nEscProp, short nEsc ) diff --git a/cui/source/tabpages/paragrph.cxx b/cui/source/tabpages/paragrph.cxx index 3e1e1aee4710..46b9a83c9690 100644 --- a/cui/source/tabpages/paragrph.cxx +++ b/cui/source/tabpages/paragrph.cxx @@ -3056,12 +3056,20 @@ SvxAsianTabPage::SvxAsianTabPage(weld::Container* pPage, weld::DialogController* , m_xHangingPunctCB(m_xBuilder->weld_check_button(u"checkHangPunct"_ustr)) , m_xScriptSpaceCB(m_xBuilder->weld_check_button(u"checkApplySpacing"_ustr)) { + SetExchangeSupport(); } SvxAsianTabPage::~SvxAsianTabPage() { } +DeactivateRC SvxAsianTabPage::DeactivatePage( SfxItemSet* pSet ) +{ + if ( pSet ) + FillItemSet( pSet ); + return DeactivateRC::LeavePage; +} + std::unique_ptr<SfxTabPage> SvxAsianTabPage::Create(weld::Container* pPage, weld::DialogController* pController, const SfxItemSet* rSet) { return std::make_unique<SvxAsianTabPage>(pPage, pController, *rSet); diff --git a/cui/source/tabpages/tptrans.cxx b/cui/source/tabpages/tptrans.cxx index 06c967f7fb2e..e9e159c6713d 100644 --- a/cui/source/tabpages/tptrans.cxx +++ b/cui/source/tabpages/tptrans.cxx @@ -458,6 +458,7 @@ bool SvxTransparenceTabPage::InitPreview(const SfxItemSet& rSet) } // Get fillstyle for preview + rSet.SetAccessTrackerIgnore(true); rXFSet.Put ( rSet.Get(XATTR_FILLSTYLE) ); rXFSet.Put ( rSet.Get(XATTR_FILLCOLOR) ); rXFSet.Put ( rSet.Get(XATTR_FILLGRADIENT) ); @@ -469,6 +470,7 @@ bool SvxTransparenceTabPage::InitPreview(const SfxItemSet& rSet) m_aCtlBitmapPreview.SetAttributes( aXFillAttr.GetItemSet() ); bBitmap = rSet.Get(XATTR_FILLSTYLE).GetValue() == drawing::FillStyle_BITMAP; + rSet.SetAccessTrackerIgnore(false); // show the right preview window if ( bBitmap ) diff --git a/include/sfx2/sfxdlg.hxx b/include/sfx2/sfxdlg.hxx index 1dedbf084496..69f0f230b45f 100644 --- a/include/sfx2/sfxdlg.hxx +++ b/include/sfx2/sfxdlg.hxx @@ -64,6 +64,11 @@ public: virtual void SetCurPageId( const OUString &rName ) = 0; virtual WhichRangesContainer GetInputRanges( const SfxItemPool& ) = 0; virtual void SetInputSet( const SfxItemSet* pInSet ) = 0; + virtual const std::set<sal_uInt16>& GetInvalidatedWhichIds() const + { + static const std::set<sal_uInt16> empty; + return empty; + } }; class SfxAbstractApplyTabDialog : virtual public SfxAbstractTabDialog diff --git a/include/sfx2/strings.hrc b/include/sfx2/strings.hrc index 88d64a425f77..573c75b0f0cc 100644 --- a/include/sfx2/strings.hrc +++ b/include/sfx2/strings.hrc @@ -231,6 +231,8 @@ #define STR_TABPAGE_INVALIDNAME NC_("STR_TABPAGE_INVALIDNAME", "This name is already in use.") #define STR_TABPAGE_INVALIDSTYLE NC_("STR_TABPAGE_INVALIDSTYLE", "This Style does not exist.") #define STR_TABPAGE_INVALIDPARENT NC_("STR_TABPAGE_INVALIDPARENT", "This Style cannot be used as a base Style, because it would result in a recursive reference.") +#define STR_RESET_PROPERTY_TOOLTIP NC_("STR_RESET_PROPERTY_TOOLTIP", "Reset this property to inherit from parent style") +#define STR_NO_MODIFIED_PROPERTIES NC_("STR_NO_MODIFIED_PROPERTIES", "All properties are inherited from parent style.") #define STR_POOL_STYLE_NAME NC_("STR_POOL_STYLE_NAME", "Name already exists as a default Style. Please choose another name.") #define STR_DELETE_STYLE_USED NC_("STR_DELETE_STYLE_USED", "One or more of the selected styles is in use in this document. If you will delete it, text or objects using these styles will revert to the parent style. Do you still wish to delete these styles? ") #define STR_DELETE_STYLE NC_("STR_DELETE_STYLE", "Styles in use: ") diff --git a/include/sfx2/tabdlg.hxx b/include/sfx2/tabdlg.hxx index 9866b5d85164..6dd6155b8162 100644 --- a/include/sfx2/tabdlg.hxx +++ b/include/sfx2/tabdlg.hxx @@ -22,6 +22,7 @@ #include <memory> #include <unordered_map> #include <string_view> +#include <set> #include <sal/config.h> #include <sfx2/dllapi.h> @@ -32,6 +33,7 @@ #include <svl/itemset.hxx> #include <svl/setitem.hxx> #include <o3tl/typed_flags_set.hxx> +#include <map> class Bitmap; class SfxTabPage; @@ -80,9 +82,11 @@ private: std::unique_ptr<SfxItemSet> m_pOutSet; std::unique_ptr<TabDlg_Impl> m_pImpl; WhichRangesContainer m_pRanges; + std::map<sal_uInt16, OUString> m_aWhichToTabMap; OUString m_sAppPageId; bool m_bStandardPushed; std::unique_ptr<SfxAllItemSet> m_xItemSet; + std::map<sal_uInt16, int> m_aWhichOrderMap; DECL_DLLPRIVATE_LINK(ActivatePageHdl, const OUString&, void); DECL_DLLPRIVATE_LINK(DeactivatePageHdl, const OUString&, bool); @@ -92,6 +96,7 @@ private: protected: virtual short Ok(); + std::set<sal_uInt16> m_aInvalidatedWhichIds; virtual void RefreshInputSet(); virtual SfxItemSet* CreateInputItemSet(const OUString& rName); virtual void PageCreated(const OUString &rName, SfxTabPage &rPage); @@ -161,6 +166,23 @@ public: void ShowPage(const OUString& rName); // SetCurPageId + call Activate on it OUString GetCurPageId() const; SfxTabPage* GetCurTabPage() const { return GetTabPage(GetCurPageId()); } + void ResetTabPage(std::u16string_view rPageId); + void ResetAllTabPages(); + void InvalidateItem(sal_uInt16 nWhich); + OUString GetTabPageNameForWhich(sal_uInt16 nWhich) const; + void BuildWhichToTabMap(); + OUString GetTabPageLabel(const OUString& rPageId) const; + std::vector<OUString> GetTabPageIds() const; + int GetWhichOrder(sal_uInt16 nWhich) const { + auto it = m_aWhichOrderMap.find(nWhich); + return it != m_aWhichOrderMap.end() ? it->second : INT_MAX; + } + const SfxItemSet* GetExampleSet() const override { return m_xExampleSet.get(); } + + /// Subclasses can override to provide additional WhichId→tab mappings + /// that the access tracker cannot determine correctly. + virtual std::map<sal_uInt16, OUString> GetWhichToTabOverrides() const { return {}; } + const std::set<sal_uInt16>& GetInvalidatedWhichIds() const { return m_aInvalidatedWhichIds; } // may provide local slots converted by Map const WhichRangesContainer& GetInputRanges( const SfxItemPool& ); @@ -181,8 +203,6 @@ public: static bool runAsync(const std::shared_ptr<SfxTabDialogController>& rController, const std::function<void(sal_Int32)>&); - virtual const SfxItemSet* GetExampleSet() const override { return m_xExampleSet.get(); } - void SetApplyHandler(const Link<weld::Button&,void>& _rHdl); //calls Ok without closing dialog diff --git a/include/svl/itemset.hxx b/include/svl/itemset.hxx index 7dd32ff77791..8e5a3b63b76d 100644 --- a/include/svl/itemset.hxx +++ b/include/svl/itemset.hxx @@ -28,6 +28,7 @@ #include <svl/poolitem.hxx> #include <svl/typedwhich.hxx> #include <svl/whichranges.hxx> +#include <set> class SfxItemPool; @@ -90,6 +91,8 @@ class SAL_WARN_UNUSED SVL_DLLPUBLIC SfxItemSet SfxItemPool* m_pPool; ///< pool that stores the items const SfxItemSet* m_pParent; ///< derivation sal_uInt16 m_nRegister; ///< number of items with NeedsSurrogateSupport + mutable std::vector<sal_uInt16>* m_pAccessTracker = nullptr; + mutable bool m_bAccessTrackerIgnore = false; #ifdef DBG_UTIL sal_uInt16 m_nRegisteredSfxItemIter; @@ -244,6 +247,8 @@ public: void SetRanges( WhichRangesContainer&& ); void MergeRange( sal_uInt16 nFrom, sal_uInt16 nTo ); const SfxItemSet* GetParent() const { return m_pParent; } + void SetAccessTracker(std::vector<sal_uInt16>* pTracker) const { m_pAccessTracker = pTracker; } + void SetAccessTrackerIgnore(bool b) const { m_bAccessTrackerIgnore = b; } bool operator==(const SfxItemSet &) const; size_t GetHashCode() const; diff --git a/include/svl/style.hxx b/include/svl/style.hxx index 2555eda5306b..f803e710b59b 100644 --- a/include/svl/style.hxx +++ b/include/svl/style.hxx @@ -34,6 +34,9 @@ #include <memory> #include <optional> +#include <vector> +#include <utility> + // This is used as a flags enum in sw/, but only there, // so I don't pull in o3tl::typed_flags here enum class SfxStyleFamily { @@ -164,6 +167,11 @@ public: virtual bool IsUsed() const; // Default true virtual OUString GetDescription( MapUnit eMetric ); + /// Returns individual property presentations for building UI chips + /// Each pair contains: WhichId and presentation string + virtual std::vector<std::pair<sal_uInt16, OUString>> GetItemPresentation( + MapUnit eMetric, const SfxItemSet* pWorkingSet = nullptr); + virtual OUString GetUsedBy() { return OUString(); } SfxStyleSheetBasePool* GetPool() { return m_pPool; } diff --git a/sc/inc/stlsheet.hxx b/sc/inc/stlsheet.hxx index 6eab3e5a1128..1e1dfef78254 100644 --- a/sc/inc/stlsheet.hxx +++ b/sc/inc/stlsheet.hxx @@ -54,6 +54,9 @@ public: /// Fix for expensive dynamic_cast virtual bool isScStyleSheet() const override { return true; } + + virtual std::vector<std::pair<sal_uInt16, OUString>> GetItemPresentation( + MapUnit eMetric, const SfxItemSet* pWorkingSet = nullptr) override; private: virtual ~ScStyleSheet() override; diff --git a/sc/inc/strings.hrc b/sc/inc/strings.hrc index c824785a830d..89ca3a06793a 100644 --- a/sc/inc/strings.hrc +++ b/sc/inc/strings.hrc @@ -445,6 +445,19 @@ #define STR_DUPLICATERECORDS_DATACONATINSROWHEADERS NC_("STR_DUPLICATERECORDS_DATACONATINSROWHEADERS", "Data contains row headers") #define STR_DUPLICATERECORDS_DATACONATINSCOLUMNHEADERS NC_("STR_DUPLICATERECORDS_DATACONATINSCOLUMNHEADERS", "Data contains column headers") +#define STR_ATTR_PROTECTION NC_("STR_ATTR_PROTECTION", "Protection") +#define STR_ATTR_ROTATION NC_("STR_ATTR_ROTATION", "Rotation") +#define STR_ATTR_ROTATION_MODE NC_("STR_ATTR_ROTATION_MODE", "Rotation Mode") +#define STR_ATTR_TEXT_DIR NC_("STR_ATTR_TEXT_DIR", "Text Direction") +#define STR_ATTR_WRAP_TEXT NC_("STR_ATTR_WRAP_TEXT", "Wrap Text") +#define STR_ATTR_SHRINK_FIT NC_("STR_ATTR_SHRINK_FIT", "Shrink to Fit") +#define STR_ATTR_STACKED NC_("STR_ATTR_STACKED", "Stacked") +#define STR_ATTR_H_ALIGN NC_("STR_ATTR_H_ALIGN", "Horizontal Alignment") +#define STR_ATTR_V_ALIGN NC_("STR_ATTR_V_ALIGN", "Vertical Alignment") +#define STR_ATTR_INDENT NC_("STR_ATTR_INDENT", "Indent") +#define STR_ATTR_SPACING NC_("STR_ATTR_SPACING", "Spacing") +#define STR_ATTR_NUM_FORMAT NC_("STR_ATTR_NUM_FORMAT", "Number Format") +#define STR_ATTR_ASIAN_LAYOUT NC_("STR_ATTR_ASIAN_LAYOUT", "Asian Layout Mode") #define STR_CONTENT_WITH_UNKNOWN_ENCRYPTION NC_("STR_CONTENT_WITH_UNKNOWN_ENCRYPTION", "Document contains DRM content that is encrypted with an unknown encryption method. Only the un-encrypted content will be shown.") diff --git a/sc/source/core/data/stlsheet.cxx b/sc/source/core/data/stlsheet.cxx index b56a6ef0c6a2..81692a994dd4 100644 --- a/sc/source/core/data/stlsheet.cxx +++ b/sc/source/core/data/stlsheet.cxx @@ -39,13 +39,33 @@ #include <svl/itemset.hxx> #include <svl/numformat.hxx> #include <svl/hint.hxx> +#include <unotools/intlwrapper.hxx> +#include <unotools/syslocale.hxx> +#include <svl/zformat.hxx> #include <o3tl/unit_conversion.hxx> #include <attrib.hxx> #include <globstr.hrc> +#include <strings.hrc> #include <scresid.hxx> #include <sc.hrc> +#include <svx/strarray.hxx> +#include <svx/svxids.hrc> +#include <svl/numformat.hxx> +#include <svl/zforlist.hxx> +#include <svl/intitem.hxx> +#include <svl/ilstitem.hxx> +#include <svl/itemiter.hxx> +#include <svl/eitem.hxx> +#include <editeng/boxitem.hxx> +#include <scitems.hxx> +#include <globstr.hrc> +#include <scresid.hxx> +#include <document.hxx> +#include <docsh.hxx> +#include <sfx2/objsh.hxx> + constexpr auto TWO_CM = o3tl::convert(2, o3tl::Length::cm, o3tl::Length::twip); // 1134 constexpr auto HFDIST_CM = o3tl::convert(250, o3tl::Length::mm100, o3tl::Length::twip); // 142 @@ -274,6 +294,175 @@ SfxItemSet& ScStyleSheet::GetItemSet() return *pSet; } +std::vector<std::pair<sal_uInt16, OUString>> ScStyleSheet::GetItemPresentation( + MapUnit eMetric, const SfxItemSet* pWorkingSet) +{ + std::vector<std::pair<sal_uInt16, OUString>> aResult; + IntlWrapper aIntlWrapper(SvtSysLocale().GetUILanguageTag()); + + const SfxItemSet& rSet = GetItemSet(); + const SfxItemSet* pCheckSet = pWorkingSet ? pWorkingSet : &rSet; + + // Get parent item set for comparison + const SfxItemSet* pParentSet = nullptr; + SfxStyleSheetBase* pParentStyle = nullptr; + if (!GetParent().isEmpty()) + { + pParentStyle = m_pPool->Find(GetParent(), GetFamily()); + if (pParentStyle) + pParentSet = &pParentStyle->GetItemSet(); + } + + // Get the number formatter for ATTR_VALUE_FORMAT + SvNumberFormatter* pFormatter = nullptr; + if (ScDocShell* pDocSh = dynamic_cast<ScDocShell*>(SfxObjectShell::Current())) + { + pFormatter = pDocSh->GetDocument().GetFormatTable(); + } + + SfxItemIter aIter(rSet); + + for (const SfxPoolItem* pItem = aIter.GetCurItem(); pItem; pItem = aIter.NextItem()) + { + if (IsInvalidItem(pItem)) + continue; + + sal_uInt16 nWhich = pItem->Which(); + + // Only show items explicitly SET in this style (not inherited) + if (pCheckSet->GetItemState(nWhich, false) != SfxItemState::SET) + continue; + + // Skip items identical to parent + if (pParentSet) + { + const SfxPoolItem* pParentItem = nullptr; + if (pParentSet->GetItemState(nWhich, true, &pParentItem) == SfxItemState::SET + && pParentItem && *pParentItem == *pItem) + continue; + } + + // Skip items equal to pool default + if (*pItem == rSet.GetPool()->GetUserOrPoolDefaultItem(nWhich)) + continue; + + // Skip internal/structural items that don't make sense as chips + switch (nWhich) + { + case SID_ATTR_BORDER_INNER: + case SID_ATTR_PARA_MODEL: + case SID_ATTR_PAGE_SIZE: + case SID_ATTR_PAGE_MAXSIZE: + case SID_ATTR_PAGE_PAPERBIN: + continue; + default: + break; + } + + OUString aItemPresentation; + + // Special handling for number format: the default GetPresentation + // just returns the raw format key (e.g. "10122") + if (nWhich == ATTR_VALUE_FORMAT && pFormatter) + { + sal_uInt32 nFormat = static_cast<const SfxUInt32Item*>(pItem)->GetValue(); + const SvNumberformat* pEntry = pFormatter->GetEntry(nFormat); + if (pEntry) + { + OUString sFormatStr = pEntry->GetFormatstring(); + // Truncate very long format strings + if (sFormatStr.getLength() > 40) + sFormatStr = OUString::Concat(sFormatStr.subView(0, 37)) + "..."; + aItemPresentation = SvxAttrNameTable::GetString( + SvxAttrNameTable::FindIndex( + rSet.GetPool()->GetSlotId(nWhich))); + if (aItemPresentation.isEmpty()) + aItemPresentation = ScResId(STR_ATTR_NUM_FORMAT); + aItemPresentation += ": " + sFormatStr; + } + else + { + continue; // Skip unresolvable format keys + } + } + else + { + // Standard presentation via pool + if (!m_pPool->GetPool().GetPresentation( + *pItem, eMetric, aItemPresentation, aIntlWrapper)) + continue; + + if (aItemPresentation.isEmpty()) + continue; + + // Add attribute name prefix if not already present + if (aItemPresentation.indexOf(": ") == -1) + { + sal_uInt16 nSlotId = rSet.GetPool()->GetSlotId(nWhich); + sal_uInt32 nIdx = SvxAttrNameTable::FindIndex(nSlotId); + OUString aAttrName = SvxAttrNameTable::GetString(nIdx); + + // Fallback for Calc-specific WhichIds not in SvxAttrNameTable + if (aAttrName.isEmpty()) + { + // You may want to add string resources in sc/inc/strings.hrc + // for now using English fallbacks that you can later replace + // with proper NC_ macros + switch (nWhich) + { + case ATTR_PROTECTION: + aAttrName = ScResId(STR_ATTR_PROTECTION); + break; + case ATTR_ROTATE_VALUE: + aAttrName = ScResId(STR_ATTR_ROTATION); + break; + case ATTR_ROTATE_MODE: + aAttrName = ScResId(STR_ATTR_ROTATION_MODE); + break; + case ATTR_WRITINGDIR: + aAttrName = ScResId(STR_ATTR_TEXT_DIR); + break; + case ATTR_LINEBREAK: + aAttrName = ScResId(STR_ATTR_WRAP_TEXT); + break; + case ATTR_SHRINKTOFIT: + aAttrName = ScResId(STR_ATTR_SHRINK_FIT); + break; + case ATTR_STACKED: + aAttrName = ScResId(STR_ATTR_STACKED); + break; + case ATTR_HOR_JUSTIFY: + aAttrName = ScResId(STR_ATTR_H_ALIGN); + break; + case ATTR_VER_JUSTIFY: + aAttrName = ScResId(STR_ATTR_V_ALIGN); + break; + case ATTR_INDENT: + aAttrName = ScResId(STR_ATTR_INDENT); + break; + case ATTR_MARGIN: + aAttrName = ScResId(STR_ATTR_SPACING); + break; + case ATTR_VERTICAL_ASIAN: + aAttrName = ScResId(STR_ATTR_ASIAN_LAYOUT); + break; + default: + break; + } + } + + if (!aAttrName.isEmpty()) + aItemPresentation = aAttrName + ": " + aItemPresentation; + } + } + + if (!aItemPresentation.isEmpty()) + aResult.emplace_back(nWhich, aItemPresentation); + } + + return aResult; +} + bool ScStyleSheet::IsUsed() const { switch (GetFamily()) diff --git a/sfx2/UIConfig_sfx.mk b/sfx2/UIConfig_sfx.mk index 79bec2ca159e..87f09452e6a6 100644 --- a/sfx2/UIConfig_sfx.mk +++ b/sfx2/UIConfig_sfx.mk @@ -56,6 +56,9 @@ $(eval $(call gb_UIConfig_add_uifiles,sfx,\ sfx2/uiconfig/ui/password \ sfx2/uiconfig/ui/notebookbarpopup \ sfx2/uiconfig/ui/printeroptionsdialog \ + sfx2/uiconfig/ui/propertychip \ + sfx2/uiconfig/ui/propertychiprow \ + sfx2/uiconfig/ui/propertycategoryrow \ sfx2/uiconfig/ui/querysavedialog \ sfx2/uiconfig/ui/quickfind \ sfx2/uiconfig/ui/saveastemplatedlg \ diff --git a/sfx2/source/dialog/mgetempl.cxx b/sfx2/source/dialog/mgetempl.cxx index f29d69041e22..264981fac84e 100644 --- a/sfx2/source/dialog/mgetempl.cxx +++ b/sfx2/source/dialog/mgetempl.cxx @@ -45,9 +45,141 @@ #include <svl/stritem.hxx> #include <sfx2/dispatch.hxx> +#include <sal/log.hxx> + +#include <algorithm> #include "mgetempl.hxx" +// PropertyChip implementation +PropertyChip::PropertyChip(weld::Box* pParent, SfxManageStyleSheetPage* pPage, + sal_uInt16 nWhich, const OUString& rText) + : m_xBuilder(Application::CreateBuilder(pParent, u"sfx/ui/propertychip.ui"_ustr)) + , m_xContainer(m_xBuilder->weld_container(u"PropertyChip"_ustr)) + , m_xLabel(m_xBuilder->weld_label(u"label"_ustr)) + , m_xRemoveBtn(m_xBuilder->weld_toolbar(u"removebar"_ustr)) + , m_pPage(pPage) + , m_sText(rText) + , m_nWhich(nWhich) +{ + // For texts longer than one line, insert manual newlines. + static constexpr int MAX_LINE_CHARS = 100; + if (rText.getLength() > MAX_LINE_CHARS) + { + OUStringBuffer aBuf; + int nLineLen = 0; + for (sal_Int32 i = 0; i < rText.getLength(); ++i) + { + sal_Unicode c = rText[i]; + aBuf.append(c); + nLineLen++; + if (nLineLen >= MAX_LINE_CHARS && (c == ' ' || c == ',' || c == ';' || c == '&')) + { + aBuf.append(' '); + nLineLen = 0; + } + } + m_xLabel->set_label(aBuf.makeStringAndClear()); + } + else + { + m_xLabel->set_label(rText); + } + m_xLabel->set_tooltip_text(rText); + m_xRemoveBtn->connect_clicked(LINK(this, PropertyChip, RemoveHdl)); +} + +PropertyChip::~PropertyChip() +{ + if (m_xContainer) + m_xContainer->set_visible(false); +} + +IMPL_LINK_NOARG(PropertyChip, RemoveHdl, const OUString&, void) +{ + m_pPage->ResetPropertyToParent(m_nWhich); +} + +// PropertyCategoryRow implementation +PropertyCategoryRow::PropertyCategoryRow(weld::Box* pParentBox, std::u16string_view rLabel) + : m_xBuilder(Application::CreateBuilder(pParentBox, u"sfx/ui/propertycategoryrow.ui"_ustr)) + , m_xContainer(m_xBuilder->weld_container(u"PropertyCategoryRow"_ustr)) + , m_xLabel(m_xBuilder->weld_label(u"label"_ustr)) + , m_xChipsBox(m_xBuilder->weld_container(u"chipsbox"_ustr)) +{ + m_xLabel->set_label(OUString::Concat(rLabel) + ":"); +} + +PropertyCategoryRow::~PropertyCategoryRow() +{ + m_aChips.clear(); + m_aChipRows.clear(); + if (m_xContainer) + m_xContainer->set_visible(false); +} + +weld::Box* PropertyCategoryRow::EnsureCurrentRow(int nChipChars) +{ + if (m_aChipRows.empty() + || (m_aChipRows.back()->nTotalChars + nChipChars > MAX_CHARS_PER_ROW + && m_aChipRows.back()->nTotalChars > 0)) + { + auto pRow = std::make_unique<ChipRow>(); + pRow->xBuilder = Application::CreateBuilder( + m_xChipsBox.get(), u"sfx/ui/propertychiprow.ui"_ustr); + pRow->xBox = pRow->xBuilder->weld_box(u"PropertyChipRow"_ustr); + pRow->xBox->show(); + pRow->nTotalChars = 0; + m_aChipRows.push_back(std::move(pRow)); + } + return m_aChipRows.back()->xBox.get(); +} + +void PropertyCategoryRow::AddChip(SfxManageStyleSheetPage* pPage, sal_uInt16 nWhich, const OUString& rText) +{ + int nTextLen = rText.getLength(); + int nChipChars = nTextLen + 3; + weld::Box* pCurrentRow = EnsureCurrentRow(nChipChars); + m_aChips.emplace_back(std::make_unique<PropertyChip>(pCurrentRow, pPage, nWhich, rText)); + m_aChipRows.back()->nTotalChars += nChipChars; +} + +void PropertyCategoryRow::RemoveChip(sal_uInt16 nWhich) +{ + struct ChipData + { + sal_uInt16 nWhich; + OUString sText; + }; + std::vector<ChipData> aData; + + SfxManageStyleSheetPage* pPage = nullptr; + for (const auto& pChip : m_aChips) + { + if (pChip->GetWhich() == nWhich) + continue; + if (!pPage) + pPage = pChip->GetPage(); + aData.push_back({pChip->GetWhich(), pChip->GetText()}); + } + + // Hide rows before destroying + for (auto& pRow : m_aChipRows) + { + if (pRow->xBox) + pRow->xBox->set_visible(false); + } + + m_aChips.clear(); + m_aChipRows.clear(); + + if (pPage) + { + for (const auto& d : aData) + AddChip(pPage, d.nWhich, d.sText); + } +} + /* SfxManageStyleSheetPage Constructor * * initializes the list box with the templates @@ -72,6 +204,10 @@ SfxManageStyleSheetPage::SfxManageStyleSheetPage(weld::Container* pPage, weld::D , m_xFilterFt(m_xBuilder->weld_label(u"categoryft"_ustr)) , m_xFilterLb(m_xBuilder->weld_combo_box(u"category"_ustr)) , m_xDescFt(m_xBuilder->weld_label(u"desc"_ustr)) + , m_xEditViewBox(m_xBuilder->weld_box(u"editviewbox"_ustr)) + , m_xEditPropsBtn(m_xBuilder->weld_toggle_button(u"editprops"_ustr)) + , m_xViewPropsBtn(m_xBuilder->weld_toggle_button(u"viewprops"_ustr)) + , m_xPropBox(m_xBuilder->weld_box(u"propbox"_ustr)) { m_xFollowLb->make_sorted(); // tdf#120188 like SwCharURLPage limit the width of the style combos @@ -258,10 +394,13 @@ SfxManageStyleSheetPage::SfxManageStyleSheetPage(weld::Container* pPage, weld::D m_xBaseLb->connect_changed(LINK(this, SfxManageStyleSheetPage, EditLinkStyleSelectHdl_Impl)); m_xEditStyleBtn->connect_clicked(LINK(this, SfxManageStyleSheetPage, EditStyleHdl_Impl)); m_xEditLinkStyleBtn->connect_clicked(LINK(this, SfxManageStyleSheetPage, EditLinkStyleHdl_Impl)); + m_xEditPropsBtn->connect_toggled(LINK(this, SfxManageStyleSheetPage, EditPropsHdl_Impl)); + m_xViewPropsBtn->connect_toggled(LINK(this, SfxManageStyleSheetPage, ViewPropsHdl_Impl)); } SfxManageStyleSheetPage::~SfxManageStyleSheetPage() { + m_aPropertyRows.clear(); pItem = nullptr; pStyle = nullptr; } @@ -299,6 +438,7 @@ void SfxManageStyleSheetPage::SetDescriptionText_Impl() /* [Description] Set attribute description. Get the set metric for this. + Also builds property chips if the style has a parent. */ { @@ -325,7 +465,195 @@ void SfxManageStyleSheetPage::SetDescriptionText_Impl() default: OSL_FAIL( "non supported field unit" ); } - m_xDescFt->set_label(pStyle->GetDescription(eUnit)); + + // If style has a parent, show description text and Edit button + if (pStyle->HasParentSupport() && !pStyle->GetParent().isEmpty()) + { + if (m_bEditMode) + { + m_xDescFt->hide(); + m_bInToggleHandler = true; + m_xEditPropsBtn->set_active(true); + m_xViewPropsBtn->set_active(false); + m_bInToggleHandler = false; + BuildPropertyChips_Impl(); + } + else + { + m_xDescFt->set_label(pStyle->GetDescription(eUnit)); + m_xDescFt->show(); + m_bInToggleHandler = true; + m_xEditPropsBtn->set_active(false); + m_xViewPropsBtn->set_active(true); + m_bInToggleHandler = false; + } + m_xEditViewBox->show(); + } + else + { + // No parent - show the full description, no Edit button + m_aPropertyRows.clear(); + m_xDescFt->set_label(pStyle->GetDescription(eUnit)); + m_xDescFt->show(); + m_xEditPropsBtn->hide(); + m_xEditViewBox->hide(); + } +} + +void SfxManageStyleSheetPage::BuildPropertyChips_Impl() + +/* [Description] + + Build property chips for all properties in this style that differ from the parent. + Each chip shows the property name/value and has an X button to reset it to parent. + Chips are grouped by tab page. +*/ + +{ + m_aPropertyRows.clear(); + + if (!pStyle->HasParentSupport() || pStyle->GetParent().isEmpty()) + return; + + MapUnit eUnit = MapUnit::MapCM; + FieldUnit eFieldUnit(FieldUnit::CM); + SfxModule* pModule = SfxModule::GetActiveModule(); + if (pModule) + { + eFieldUnit = pModule->GetFieldUnit(); + } + + switch (eFieldUnit) + { + case FieldUnit::MM: eUnit = MapUnit::MapMM; break; + case FieldUnit::CM: + case FieldUnit::M: + case FieldUnit::KM: eUnit = MapUnit::MapCM; break; + case FieldUnit::POINT: + case FieldUnit::PICA: eUnit = MapUnit::MapPoint; break; + case FieldUnit::INCH: + case FieldUnit::FOOT: + case FieldUnit::MILE: eUnit = MapUnit::MapInch; break; + default: + break; + } + + // Get the dialog controller to find tab page names + SfxTabDialogController* pDlgController = static_cast<SfxTabDialogController*>(GetDialogController()); + if (pDlgController) + pDlgController->BuildWhichToTabMap(); + + // Use the virtual method to get individual property presentations + const SfxItemSet* pWorkingSet = pDlgController ? pDlgController->GetExampleSet() : nullptr; + std::vector<std::pair<sal_uInt16, OUString>> aItems = pStyle->GetItemPresentation(eUnit, pWorkingSet); + + // Group items by tab page + std::map<OUString, std::vector<std::pair<sal_uInt16, OUString>>> aGroupedItems; + + for (const auto& rItem : aItems) + { + // Skip items that user has explicitly reset + if (m_aResetWhichIds.find(rItem.first) != m_aResetWhichIds.end()) + continue; + + OUString sTabId; + if (pDlgController) + { + sTabId = pDlgController->GetTabPageNameForWhich(rItem.first); + } + + if (sTabId.isEmpty()) + { + continue; // skip unclassifiable items + } + + aGroupedItems[sTabId].push_back(rItem); + } + + // Create category rows in tab order (not alphabetical) + std::vector<OUString> aTabOrder; + if (pDlgController) + { + aTabOrder = pDlgController->GetTabPageIds(); + } + // Add "other" at the end for items not matching any tab + aTabOrder.push_back("other"); + + for (const auto& sTabId : aTabOrder) + { + auto itGroup = aGroupedItems.find(sTabId); + if (itGroup == aGroupedItems.end()) + continue; // No items for this tab + + OUString sLabel; + if (pDlgController && sTabId != "other") + { + sLabel = pDlgController->GetTabPageLabel(sTabId); + } + if (sLabel.isEmpty()) + sLabel = sTabId; + + auto pRow = std::make_unique<PropertyCategoryRow>(m_xPropBox.get(), sLabel); + + auto& aItemVec = itGroup->second; + std::sort(aItemVec.begin(), aItemVec.end(), + [&](const auto& a, const auto& b) { + auto itA = pDlgController->GetWhichOrder(a.first); + auto itB = pDlgController->GetWhichOrder(b.first); + return itA < itB; + }); + + for (const auto& rItem : itGroup->second) + { + pRow->AddChip(this, rItem.first, rItem.second); + } + + m_aPropertyRows[sTabId] = std::move(pRow); + } + + for (const auto& sTabId : aTabOrder) + { + auto it = m_aPropertyRows.find(sTabId); + if (it != m_aPropertyRows.end()) + it->second->Show(); + } + + if (m_aPropertyRows.empty()) + { + m_xDescFt->set_label(SfxResId(STR_NO_MODIFIED_PROPERTIES)); + m_xDescFt->show(); + } +} + +void SfxManageStyleSheetPage::ResetPropertyToParent(sal_uInt16 nWhich) +{ + SfxItemSet& rSet = pStyle->GetItemSet(); + + // Clear this item to restore inheritance + rSet.ClearItem(nWhich); + + // Track that user has reset this property + m_aResetWhichIds.insert(nWhich); + + bModified = true; + + // Invalidate this item in the dialog's output set + SfxTabDialogController* pDlgController = static_cast<SfxTabDialogController*>(GetDialogController()); + if (pDlgController) + { + pDlgController->InvalidateItem(nWhich); + } + + // Rebuild all chips from scratch — this avoids FlowBox residue issues + m_aPropertyRows.clear(); + BuildPropertyChips_Impl(); + + // If no rows left, show the "all inherited" message + if (m_aPropertyRows.empty()) + { + m_xDescFt->set_label(SfxResId(STR_NO_MODIFIED_PROPERTIES)); + m_xDescFt->show(); + } } IMPL_LINK_NOARG(SfxManageStyleSheetPage, EditStyleSelectHdl_Impl, weld::ComboBox&, void) @@ -357,6 +685,38 @@ IMPL_LINK_NOARG(SfxManageStyleSheetPage, EditLinkStyleHdl_Impl, weld::Button&, v Execute_Impl( SID_STYLE_EDIT, aTemplName, static_cast<sal_uInt16>(pStyle->GetFamily()) ); } +IMPL_LINK_NOARG(SfxManageStyleSheetPage, EditPropsHdl_Impl, weld::Toggleable&, void) +{ + if (m_bInToggleHandler) + return; + m_bInToggleHandler = true; + + m_xEditPropsBtn->set_active(true); + m_xViewPropsBtn->set_active(false); + + m_bEditMode = true; + m_xDescFt->hide(); + BuildPropertyChips_Impl(); + + m_bInToggleHandler = false; +} + +IMPL_LINK_NOARG(SfxManageStyleSheetPage, ViewPropsHdl_Impl, weld::Toggleable&, void) +{ + if (m_bInToggleHandler) + return; + m_bInToggleHandler = true; + + m_xViewPropsBtn->set_active(true); + m_xEditPropsBtn->set_active(false); + + m_bEditMode = false; + m_aPropertyRows.clear(); + SetDescriptionText_Impl(); + + m_bInToggleHandler = false; +} + // Internal: Perform functions through the Dispatcher bool SfxManageStyleSheetPage::Execute_Impl( sal_uInt16 nId, const OUString &rStr, sal_uInt16 nFamily) @@ -454,7 +814,6 @@ bool SfxManageStyleSheetPage::FillItemSet( SfxItemSet* rSet ) return bModified; } - void SfxManageStyleSheetPage::Reset( const SfxItemSet* /*rAttrSet*/ ) /* [Description] @@ -555,6 +914,14 @@ void SfxManageStyleSheetPage::ActivatePage( const SfxItemSet& rSet) */ { + // Rebuild property chips when returning to this page in edit mode, + // so that changes made in other tab pages are reflected immediately. + if (m_bEditMode) + { + m_aPropertyRows.clear(); + BuildPropertyChips_Impl(); + } + SetDescriptionText_Impl(); // It is a style with auto update? (SW only) diff --git a/sfx2/source/dialog/mgetempl.hxx b/sfx2/source/dialog/mgetempl.hxx index 0e59c6862ef8..f5eaa8fa5253 100644 --- a/sfx2/source/dialog/mgetempl.hxx +++ b/sfx2/source/dialog/mgetempl.hxx @@ -21,7 +21,12 @@ #include <sfx2/styfitem.hxx> #include <sfx2/tabdlg.hxx> +#include <vcl/weld/Toolbar.hxx> #include <memory> +#include <vector> +#include <algorithm> +#include <map> +#include <set> namespace weld { class Button; } namespace weld { class CheckButton; } @@ -30,11 +35,73 @@ namespace weld { class Entry; } namespace weld { class Label; } namespace weld { class Widget; } +namespace weld { class Container; } +namespace weld { class Box; } + +class SfxManageStyleSheetPage; + +/// A "chip" widget representing a single style property that can be reset to parent +class PropertyChip final +{ +private: + std::unique_ptr<weld::Builder> m_xBuilder; + std::unique_ptr<weld::Container> m_xContainer; + std::unique_ptr<weld::Label> m_xLabel; + std::unique_ptr<weld::Toolbar> m_xRemoveBtn;; + + SfxManageStyleSheetPage* m_pPage; + OUString m_sText; + sal_uInt16 m_nWhich; + + DECL_LINK(RemoveHdl, const OUString&, void); + +public: + PropertyChip(weld::Box* pParent, SfxManageStyleSheetPage* pPage, + sal_uInt16 nWhich, const OUString& rText); + ~PropertyChip(); + + sal_uInt16 GetWhich() const { return m_nWhich; } + const OUString& GetText() const { return m_sText; } + SfxManageStyleSheetPage* GetPage() const { return m_pPage; } +}; /* expected: SID_TEMPLATE_NAME : In: StringItem, Name of Template SID_TEMPLATE_FAMILY : In: Family of Template */ +/// A row containing chips for a single tab/category +class PropertyCategoryRow final +{ +private: + std::unique_ptr<weld::Builder> m_xBuilder; + std::unique_ptr<weld::Container> m_xContainer; + std::unique_ptr<weld::Label> m_xLabel; + std::unique_ptr<weld::Container> m_xChipsBox; + std::vector<std::unique_ptr<PropertyChip>> m_aChips; + + struct ChipRow + { + std::unique_ptr<weld::Builder> xBuilder; + std::unique_ptr<weld::Box> xBox; + int nTotalChars = 0; // estimated total character width + }; + std::vector<std::unique_ptr<ChipRow>> m_aChipRows; + static constexpr int MAX_CHARS_PER_ROW = 100; + + weld::Box* EnsureCurrentRow(int nChipChars); + +public: + PropertyCategoryRow(weld::Box* pParentBox, std::u16string_view rLabel); + ~PropertyCategoryRow(); + void AddChip(SfxManageStyleSheetPage* pPage, sal_uInt16 nWhich, const OUString& rText); + void RemoveChip(sal_uInt16 nWhich); + bool IsEmpty() const { return m_aChips.empty(); } + void Show() { if (m_xContainer) m_xContainer->set_visible(true); } + void Hide() { if (m_xContainer) m_xContainer->set_visible(false); } + OUString GetLabel() const { return m_xLabel->get_label(); } + int GetChipRowCount() const { return static_cast<int>(m_aChipRows.size()); } +}; + class SfxManageStyleSheetPage final : public SfxTabPage { SfxStyleSheetBase *pStyle; @@ -60,8 +127,17 @@ class SfxManageStyleSheetPage final : public SfxTabPage std::unique_ptr<weld::Label> m_xFilterFt; std::unique_ptr<weld::ComboBox> m_xFilterLb; std::unique_ptr<weld::Label> m_xDescFt; + std::unique_ptr<weld::Box> m_xEditViewBox; + std::unique_ptr<weld::Toggleable> m_xEditPropsBtn; + std::unique_ptr<weld::Toggleable> m_xViewPropsBtn; + std::unique_ptr<weld::Box> m_xPropBox; + std::map<OUString, std::unique_ptr<PropertyCategoryRow>> m_aPropertyRows; + std::set<sal_uInt16> m_aResetWhichIds; // Track Which IDs that user has reset + bool m_bEditMode = false; // True after user clicks Edit + bool m_bInToggleHandler = false; friend class SfxStyleDialogController; + friend class PropertyChip; DECL_LINK(GetFocusHdl, weld::Widget&, void); DECL_LINK(LoseFocusHdl, weld::Widget&, void); @@ -69,18 +145,20 @@ class SfxManageStyleSheetPage final : public SfxTabPage DECL_LINK(EditStyleHdl_Impl, weld::Button&, void); DECL_LINK(EditLinkStyleSelectHdl_Impl, weld::ComboBox&, void); DECL_LINK(EditLinkStyleHdl_Impl, weld::Button&, void); - + DECL_LINK(EditPropsHdl_Impl, weld::Toggleable&, void); + DECL_LINK(ViewPropsHdl_Impl, weld::Toggleable&, void); void UpdateName_Impl(weld::ComboBox*, const OUString &rNew); void SetDescriptionText_Impl(); - + void BuildPropertyChips_Impl(); + void ResetPropertyToParent(sal_uInt16 nWhich); + virtual void ActivatePage(const SfxItemSet& rSet) override; static std::unique_ptr<SfxTabPage> Create( weld::Container* pPage, weld::DialogController* pController, const SfxItemSet* ); virtual bool FillItemSet(SfxItemSet *) override; virtual void Reset(const SfxItemSet *) override; static bool Execute_Impl( sal_uInt16 nId, const OUString& rStr, sal_uInt16 nFamily ); - virtual void ActivatePage(const SfxItemSet &) override; virtual DeactivateRC DeactivatePage(SfxItemSet *) override; public: diff --git a/sfx2/source/dialog/styledlg.cxx b/sfx2/source/dialog/styledlg.cxx index 850c10bf3607..489cd69a84c2 100644 --- a/sfx2/source/dialog/styledlg.cxx +++ b/sfx2/source/dialog/styledlg.cxx @@ -81,6 +81,13 @@ SfxStyleDialogController::~SfxStyleDialogController() short SfxStyleDialogController::Ok() { SfxTabDialogController::Ok(); + + SfxItemSet& rStyleSet = m_rStyle.GetItemSet(); + for (sal_uInt16 nWhich : m_aInvalidatedWhichIds) + { + rStyleSet.ClearItem(nWhich); + } + return RET_OK; } diff --git a/sfx2/source/dialog/tabdlg.cxx b/sfx2/source/dialog/tabdlg.cxx index efb2fba57831..c17018d48987 100644 --- a/sfx2/source/dialog/tabdlg.cxx +++ b/sfx2/source/dialog/tabdlg.cxx @@ -37,9 +37,15 @@ #include <sal/log.hxx> #include <tools/debug.hxx> #include <comphelper/lok.hxx> +#include <editeng/editids.hrc> +#include <svx/xdef.hxx> #include <sfx2/strings.hrc> #include <helpids.h> +#include <map> +#include <set> + + using namespace ::com::sun::star::uno; @@ -60,6 +66,7 @@ namespace { struct Data_Impl { OUString sId; // The ID + OUString sLabel; // The tab label CreateTabPage fnCreatePage; // Pointer to Factory GetTabPageRanges fnGetRanges; // Pointer to Ranges-Function std::unique_ptr<SfxTabPage> xTabPage; // The TabPage itself @@ -595,7 +602,6 @@ bool SfxTabDialogController::DeactivatePage(std::u16string_view aPage) if (m_pSet) { SfxItemSet aTmp( *m_pSet->GetPool(), m_pSet->GetRanges() ); - if (pPage->HasExchangeSupport()) nRet = pPage->DeactivatePage(&aTmp); else @@ -605,6 +611,12 @@ bool SfxTabDialogController::DeactivatePage(std::u16string_view aPage) { m_xExampleSet->Put( aTmp ); m_pOutSet->Put( aTmp ); + + // Re-clear any items that were explicitly invalidated by user + for (sal_uInt16 nWhich : m_aInvalidatedWhichIds) + { + m_xExampleSet->ClearItem(nWhich); + } } } else @@ -817,6 +829,16 @@ short SfxTabDialogController::Ok() if (m_bStandardPushed) bModified = true; + if (!m_aInvalidatedWhichIds.empty()) + bModified = true; + + // Re-apply user-requested property resets after FillItemSet + for (sal_uInt16 nWhich : m_aInvalidatedWhichIds) + { + if (m_pOutSet) + m_pOutSet->ClearItem(nWhich); + } + return bModified ? RET_OK : RET_CANCEL; } @@ -877,11 +899,9 @@ void SfxTabDialogController::AddTabPage(const OUString &rName /* Page ID */, } /* [Description] - Add a page to the dialog. The Rider text is passed on, the page has no counterpart in the TabControl in the resource of the dialogue. */ - void SfxTabDialogController::AddTabPage(const OUString &rName, /* Page ID */ const OUString& rRiderText, CreateTabPage pCreateFunc, /* Pointer to the Factory Method */ @@ -890,6 +910,10 @@ void SfxTabDialogController::AddTabPage(const OUString &rName, /* Page ID */ assert(!m_xTabCtrl->get_page(rName) && "Double Page-Ids in the Tabpage"); AddTabPage(rName, pCreateFunc, nullptr); m_xTabCtrl->append_page(rName, rRiderText, pIconName); + // Save the label in Data_Impl + auto it = Find(m_pImpl->aData, rName); + if (it != m_pImpl->aData.end()) + (*it)->sLabel = rRiderText; } void SfxTabDialogController::AddTabPage(const OUString &rName, /* Page ID */ @@ -901,6 +925,10 @@ void SfxTabDialogController::AddTabPage(const OUString &rName, /* Page ID */ assert(!m_xTabCtrl->get_page(rName) && "Double Page-Ids in the Tabpage"); AddTabPage(rName, pCreateFunc, pRangesFunc); m_xTabCtrl->append_page(rName, rRiderText, &rIconName); + // Save the label in Data_Impl + auto it = Find(m_pImpl->aData, rName); + if (it != m_pImpl->aData.end()) + (*it)->sLabel = rRiderText; } void SfxTabDialogController::AddTabPage(const OUString &rName, @@ -918,6 +946,10 @@ void SfxTabDialogController::AddTabPage(const OUString &rName, const OUString& r assert(!m_xTabCtrl->get_page(rName) && "Double Page-Ids in the Tabpage"); AddTabPage(rName, nPageCreateId); m_xTabCtrl->append_page(rName, rRiderText, pIconName); + // Save the label in Data_Impl + auto it = Find(m_pImpl->aData, rName); + if (it != m_pImpl->aData.end()) + (*it)->sLabel = rRiderText; } void SfxTabDialogController::AddTabPage(const OUString &rName, const OUString& rRiderText, @@ -1036,7 +1068,6 @@ void SfxTabDialogController::RemoveTabPage(const OUString& rId) void SfxTabDialogController::Start_Impl() { CreatePages(); - setPreviewsToSamePlace(); assert(m_pImpl->aData.size() == static_cast<size_t>(m_xTabCtrl->get_n_pages()) @@ -1083,6 +1114,204 @@ OUString SfxTabDialogController::GetCurPageId() const return m_xTabCtrl->get_current_page_ident(); } +void SfxTabDialogController::ResetTabPage(std::u16string_view rPageId) +{ + SfxTabPage* pPage = GetTabPage(rPageId); + if (pPage) + { + const SfxItemSet* pSet = m_xExampleSet ? m_xExampleSet.get() : m_pSet.get(); + if (pSet) + { + pPage->Reset(pSet); + } + } +} + +void SfxTabDialogController::ResetAllTabPages() +{ + const SfxItemSet* pSet = m_xExampleSet ? m_xExampleSet.get() : m_pSet.get(); + if (!pSet) + return; + + for (auto const& elem : m_pImpl->aData) + { + if (elem->xTabPage) + { + elem->xTabPage->Reset(pSet); + } + } +} + +void SfxTabDialogController::InvalidateItem(sal_uInt16 nWhich) +{ + m_aInvalidatedWhichIds.insert(nWhich); + + if (m_xExampleSet) + { + m_xExampleSet->ClearItem(nWhich); + } + if (m_pOutSet) + m_pOutSet->InvalidateItem(nWhich); + + if (!m_pSet) + return; + + const SfxItemPool* pPool = m_pSet->GetPool(); + if (!pPool) + return; + + sal_uInt16 nSlotId = pPool->GetSlotId(nWhich); + + for (auto const& elem : m_pImpl->aData) + { + if (!elem->xTabPage) + continue; + + bool bFound = false; + + if (elem->fnGetRanges) + { + const WhichRangesContainer aRanges = elem->fnGetRanges(); + + for (const auto& rPair : aRanges) + { + // Check 1: via SlotId + if (nSlotId >= rPair.first && nSlotId <= rPair.second) + { + bFound = true; + break; + } + + // Check 2: iterate each slot, convert to WhichId + for (sal_uInt16 nSlot = rPair.first; nSlot <= rPair.second; ++nSlot) + { + if (pPool->GetWhichIDFromSlotID(nSlot) == nWhich) + { + bFound = true; + break; + } + } + if (bFound) + break; + } + } + + if (bFound) + { + const SfxItemSet* pResetSet = m_xExampleSet ? m_xExampleSet.get() : m_pSet.get(); + if (pResetSet) + { + elem->xTabPage->Reset(pResetSet); + } + } + else + { + elem->bRefresh = true; + } + } +} + +void SfxTabDialogController::BuildWhichToTabMap() +{ + if (!m_aWhichToTabMap.empty()) + return; + + const SfxItemSet* pSet = m_xExampleSet ? m_xExampleSet.get() : m_pSet.get(); + if (!pSet) + return; + + const SfxItemPool* pPool = m_pSet ? m_pSet->GetPool() : nullptr; + + // Passata 1: tracker (copre le tab che accedono via Get/GetItemState) + for (auto const& elem : m_pImpl->aData) + { + if (!elem->xTabPage) + continue; + + std::vector<sal_uInt16> aAccessed; + pSet->SetAccessTracker(&aAccessed); + elem->xTabPage->Reset(pSet); + pSet->SetAccessTracker(nullptr); + + for (int i = 0; i < static_cast<int>(aAccessed.size()); ++i) + { + sal_uInt16 nWhich = aAccessed[i]; + if (m_aWhichToTabMap.find(nWhich) == m_aWhichToTabMap.end()) + { + m_aWhichToTabMap[nWhich] = elem->sId; + m_aWhichOrderMap[nWhich] = i; + } + } + } + + // Passata 2: GetRanges (copre le tab che accedono ItemSet interni) + if (pPool) + { + for (auto const& elem : m_pImpl->aData) + { + if (!elem->fnGetRanges) + continue; + + const WhichRangesContainer aRanges = elem->fnGetRanges(); + for (const auto& rPair : aRanges) + { + for (sal_uInt16 nSlot = rPair.first; nSlot <= rPair.second; ++nSlot) + { + sal_uInt16 nWhich = pPool->GetWhichIDFromSlotID(nSlot); + // Solo se non già classificato dal tracker + if (m_aWhichToTabMap.find(nWhich) == m_aWhichToTabMap.end()) + m_aWhichToTabMap[nWhich] = elem->sId; + } + } + } + } + + // Restore delle tab page + for (auto const& elem : m_pImpl->aData) + { + if (!elem->xTabPage) + continue; + if (elem->xTabPage->DeferResetToFirstActivation()) + elem->bRefresh = true; + else + elem->xTabPage->Reset(pSet); + } +} + +OUString SfxTabDialogController::GetTabPageNameForWhich(sal_uInt16 nWhich) const +{ + auto it = m_aWhichToTabMap.find(nWhich); + if (it != m_aWhichToTabMap.end()) + return it->second; + return OUString(); +} + +OUString SfxTabDialogController::GetTabPageLabel(const OUString& rPageId) const +{ + // First try to get the label from Data_Impl + auto it = Find(m_pImpl->aData, rPageId); + if (it != m_pImpl->aData.end() && !(*it)->sLabel.isEmpty()) + return (*it)->sLabel; + + // Then try from the notebook + OUString sLabel = m_xTabCtrl->get_tab_label_text(rPageId); + if (!sLabel.isEmpty()) + return sLabel; + + // Last resort: return the page ID itself + return rPageId; +} + +std::vector<OUString> SfxTabDialogController::GetTabPageIds() const +{ + std::vector<OUString> aResult; + for (auto const& elem : m_pImpl->aData) + { + aResult.push_back(elem->sId); + } + return aResult; +} + short SfxTabDialogController::run() { Start_Impl(); diff --git a/sfx2/uiconfig/ui/managestylepage.ui b/sfx2/uiconfig/ui/managestylepage.ui index 59e6e91fcfd4..42f07abad6e6 100644 --- a/sfx2/uiconfig/ui/managestylepage.ui +++ b/sfx2/uiconfig/ui/managestylepage.ui @@ -245,18 +245,115 @@ <property name="label-xalign">0</property> <property name="shadow-type">none</property> <child> - <object class="GtkLabel" id="desc"> + <object class="GtkBox" id="containsbox"> <property name="visible">True</property> <property name="can-focus">False</property> - <property name="margin-start">12</property> + <property name="margin-start">24</property> <property name="margin-top">6</property> - <property name="wrap">True</property> - <property name="max-width-chars">52</property> - <property name="xalign">0</property> - <property name="yalign">0</property> - <attributes> - <attribute name="scale" value="0.90000000000000002"/> - </attributes> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="desc"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="wrap">True</property> + <property name="max-width-chars">52</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <attributes> + <attribute name="scale" value="0.90000000000000002"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="propscroll"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="hscrollbar-policy">never</property> + <property name="vscrollbar-policy">automatic</property> + <property name="shadow-type">none</property> + <property name="min-content-height">60</property> + <property name="max-content-height">200</property> + <child> + <object class="GtkViewport" id="propviewport"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkBox" id="propbox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <property name="spacing">4</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="editviewbox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">horizontal</property> + <property name="spacing">0</property> + <property name="halign">start</property> + <property name="margin-top">6</property> + <child> + <object class="GtkToggleButton" id="viewprops"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="label" translatable="yes" context="managestylepage|viewprops">View</property> + <child internal-child="accessible"> + <object class="AtkObject" id="viewprops-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes" context="managestylepage|viewprops">View style properties as description text</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="editprops"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="label" translatable="yes" context="managestylepage|editprops">Edit</property> + <child internal-child="accessible"> + <object class="AtkObject" id="editprops-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes" context="managestylepage|editprops">Edit style properties as interactive chips</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> </object> </child> <child type="label"> @@ -271,7 +368,7 @@ </child> </object> <packing> - <property name="expand">False</property> + <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> diff --git a/sfx2/uiconfig/ui/propertycategoryrow.ui b/sfx2/uiconfig/ui/propertycategoryrow.ui new file mode 100644 index 000000000000..08315690d9e0 --- /dev/null +++ b/sfx2/uiconfig/ui/propertycategoryrow.ui @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="sfx"> + <requires lib="gtk+" version="3.24"/> + <object class="GtkBox" id="PropertyCategoryRow"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">0</property> + <property name="margin-bottom">2</property> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <property name="label">Category:</property> + <child internal-child="accessible"> + <object class="AtkObject" id="label-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes" context="propertycategoryrow|label">Property category name</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="chipsbox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">0</property> + <property name="margin-start">12</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </object> +</interface> diff --git a/sfx2/uiconfig/ui/propertychip.ui b/sfx2/uiconfig/ui/propertychip.ui new file mode 100644 index 000000000000..0628e6b066e6 --- /dev/null +++ b/sfx2/uiconfig/ui/propertychip.ui @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="sfx"> + <requires lib="gtk+" version="3.24"/> + <object class="GtkBox" id="PropertyChip"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">horizontal</property> + <property name="spacing">2</property> + <child> + <object class="GtkToolbar" id="removebar"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="toolbar-style">icons</property> + <property name="show-arrow">False</property> + <property name="icon_size">2</property> + <property name="valign">start</property> + <child> + <object class="GtkToolButton" id="remove"> + <property name="visible">True</property> + <property name="icon-name">window-close-symbolic</property> + <property name="tooltip-text" translatable="yes" context="propertychip|remove">Reset this property to inherit from parent style</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">False</property> + </packing> + </child> + <style> + <class name="small-button"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="ellipsize">none</property> + <property name="xalign">0</property> + <attributes> + <attribute name="scale" value="0.90000000000000002"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> +</interface> diff --git a/sfx2/uiconfig/ui/propertychiprow.ui b/sfx2/uiconfig/ui/propertychiprow.ui new file mode 100644 index 000000000000..e4c5438975b5 --- /dev/null +++ b/sfx2/uiconfig/ui/propertychiprow.ui @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="sfx"> + <requires lib="gtk+" version="3.24"/> + <object class="GtkBox" id="PropertyChipRow"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">horizontal</property> + <property name="spacing">8</property> + </object> +</interface> diff --git a/solenv/sanitizers/ui/sfx.suppr b/solenv/sanitizers/ui/sfx.suppr index 58fcd42eb62d..0996e9dd18d3 100644 --- a/solenv/sanitizers/ui/sfx.suppr +++ b/solenv/sanitizers/ui/sfx.suppr @@ -55,3 +55,5 @@ sfx2/uiconfig/ui/password.ui://GtkLevelBar[@id='pass1bar'] no-labelled-by sfx2/uiconfig/ui/password.ui://GtkLabel[@id='pass1policylabel'] orphan-label sfx2/uiconfig/ui/password.ui://GtkLevelBar[@id='pass2bar'] no-labelled-by sfx2/uiconfig/ui/password.ui://GtkLabel[@id='pass2policylabel'] orphan-label +sfx2/uiconfig/ui/propertychip.ui://GtkLabel[@id='label'] orphan-label +sfx2/uiconfig/ui/propertycategoryrow.ui://GtkLabel[@id='label'] orphan-label diff --git a/svl/source/items/itemset.cxx b/svl/source/items/itemset.cxx index 1911fcb3b82c..b2a8ad0e6f24 100644 --- a/svl/source/items/itemset.cxx +++ b/svl/source/items/itemset.cxx @@ -496,6 +496,12 @@ SfxItemState SfxItemSet::GetItemState_ForIter(PoolItemMap::const_iterator aHit, SfxItemState SfxItemSet::GetItemState_ForWhichID( SfxItemState eState, sal_uInt16 nWhich, bool bSrchInParent, const SfxPoolItem **ppItem) const { + if (m_pAccessTracker && !m_bAccessTrackerIgnore) { + if (std::find(m_pAccessTracker->begin(), m_pAccessTracker->end(), nWhich) + == m_pAccessTracker->end()) + m_pAccessTracker->push_back(nWhich); + } + PoolItemMap::const_iterator aHit(m_aPoolItemMap.find(nWhich)); if (aHit != m_aPoolItemMap.end()) @@ -519,6 +525,12 @@ SfxItemState SfxItemSet::GetItemState_ForWhichID( SfxItemState eState, sal_uInt1 bool SfxItemSet::HasItem(sal_uInt16 nWhich, const SfxPoolItem** ppItem) const { + if (m_pAccessTracker && !m_bAccessTrackerIgnore) { + if (std::find(m_pAccessTracker->begin(), m_pAccessTracker->end(), nWhich) + == m_pAccessTracker->end()) + m_pAccessTracker->push_back(nWhich); + } + const bool bRet(SfxItemState::SET == GetItemState_ForWhichID(SfxItemState::UNKNOWN, nWhich, true, ppItem)); // we need to reset ppItem when it was *not* set by GetItemState_ForWhichID @@ -921,6 +933,12 @@ const SfxPoolItem* SfxItemSet::GetItem(sal_uInt16 nId, bool bSearchInParent) con const SfxPoolItem& SfxItemSet::Get( sal_uInt16 nWhich, bool bSrchInParent) const { + if (m_pAccessTracker && !m_bAccessTrackerIgnore) { + if (std::find(m_pAccessTracker->begin(), m_pAccessTracker->end(), nWhich) + == m_pAccessTracker->end()) + m_pAccessTracker->push_back(nWhich); + } + PoolItemMap::const_iterator aHit(m_aPoolItemMap.find(nWhich)); if (aHit != m_aPoolItemMap.end()) diff --git a/svl/source/items/style.cxx b/svl/source/items/style.cxx index 594736cc43b1..441d756f4beb 100644 --- a/svl/source/items/style.cxx +++ b/svl/source/items/style.cxx @@ -364,6 +364,55 @@ OUString SfxStyleSheetBase::GetDescription( MapUnit eMetric ) return aDesc.makeStringAndClear(); } +std::vector<std::pair<sal_uInt16, OUString>> SfxStyleSheetBase::GetItemPresentation(MapUnit eMetric, const SfxItemSet* /*pWorkingSet*/) +{ + std::vector<std::pair<sal_uInt16, OUString>> aResult; + SfxItemIter aIter(GetItemSet()); + IntlWrapper aIntlWrapper(SvtSysLocale().GetUILanguageTag()); + + // Get parent item set for comparison + const SfxItemSet* pParentSet = nullptr; + SfxStyleSheetBase* pParentStyle = nullptr; + if (!GetParent().isEmpty()) + { + pParentStyle = m_pPool->Find(GetParent(), GetFamily()); + if (pParentStyle) + pParentSet = &pParentStyle->GetItemSet(); + } + + for (const SfxPoolItem* pItem = aIter.GetCurItem(); pItem; pItem = aIter.NextItem()) + { + if (IsInvalidItem(pItem)) + continue; + + sal_uInt16 nWhich = pItem->Which(); + + // Only include items that are set in this style (not inherited) + if (GetItemSet().GetItemState(nWhich, false) != SfxItemState::SET) + continue; + + // Skip items identical to parent + if (pParentSet) + { + const SfxPoolItem* pParentItem = nullptr; + if (pParentSet->GetItemState(nWhich, true, &pParentItem) == SfxItemState::SET + && pParentItem && *pParentItem == *pItem) + continue; + } + + OUString aItemPresentation; + if (m_pPool->GetPool().GetPresentation(*pItem, eMetric, aItemPresentation, aIntlWrapper)) + { + if (!aItemPresentation.isEmpty()) + { + + aResult.emplace_back(nWhich, aItemPresentation); + } + } + } + return aResult; +} + inline bool SfxStyleSheetIterator::IsTrivialSearch() const { return (( nMask & SfxStyleSearchBits::AllVisible ) == SfxStyleSearchBits::AllVisible) && diff --git a/sw/inc/docstyle.hxx b/sw/inc/docstyle.hxx index 1f2bffef3ac7..fbc0670d7973 100644 --- a/sw/inc/docstyle.hxx +++ b/sw/inc/docstyle.hxx @@ -132,6 +132,9 @@ public: virtual bool HasParentSupport() const override; virtual bool HasClearParentSupport() const override; virtual OUString GetDescription(MapUnit eUnit) override; + virtual std::vector<std::pair<sal_uInt16, OUString>> GetItemPresentation( + MapUnit eMetric, const SfxItemSet* pWorkingSet = nullptr) override; + void ResetItems(const std::set<sal_uInt16>& rWhichIds); virtual OUString GetUsedBy() override; diff --git a/sw/inc/strings.hrc b/sw/inc/strings.hrc index 9bc2d7db19f9..cd0160b28e72 100644 --- a/sw/inc/strings.hrc +++ b/sw/inc/strings.hrc @@ -1555,6 +1555,20 @@ #define STR_UNDO_MAKE_ENDNOTES_FOOTNOTES NC_("STR_UNDO_MAKE_ENDNOTES_FOOTNOTES", "Make all endnotes footnotes") #define STR_UNDO_CONVERT_FIELD_TO_TEXT NC_("STR_UNDO_CONVERT_FIELD_TO_TEXT", "Convert field to text") +// Attribute names for style property chips in the Organizer tab. +// These labels are shown alongside property values when a style overrides +// a property from its parent (e.g. "Outline Level: 6", "Fill Style: Gradient"). +// They are needed for Writer-specific attributes not covered by SvxAttrNameTable. +#define STR_ATTR_OUTLINE_LEVEL NC_("STR_ATTR_OUTLINE_LEVEL", "Outline Level") +#define STR_ATTR_LIST_LEVEL NC_("STR_ATTR_LIST_LEVEL", "List Level") +#define STR_ATTR_LIST_RESTART NC_("STR_ATTR_LIST_RESTART", "List Restart") +#define STR_ATTR_LIST_RESTART_VALUE NC_("STR_ATTR_LIST_RESTART_VALUE", "List Restart Value") +#define STR_ATTR_TEXT_DIRECTION NC_("STR_ATTR_TEXT_DIRECTION", "Text Direction") +#define STR_ATTR_HIDDEN NC_("STR_ATTR_HIDDEN", "Hidden") +#define STR_ATTR_FILL_STYLE NC_("STR_ATTR_FILL_STYLE", "Fill Style") +#define STR_ATTR_GRADIENT NC_("STR_ATTR_GRADIENT", "Gradient") +#define STR_ATTR_GRADIENT_STEPS NC_("STR_ATTR_GRADIENT_STEPS", "Gradient Steps") + // To translators: title, text, question for confirmation whether to switch overwrite mode on #define STR_QUERY_INSMODE_TITLE NC_("STR_QUERY_INSMODE_TITLE", "You are switching to the overwrite mode") #define STR_QUERY_INSMODE_TEXT NC_("STR_QUERY_INSMODE_TEXT", "The overwrite mode allows to type over text. It is indicated by a block cursor and at the statusbar. Press Insert again to switch back.") diff --git a/sw/qa/uitest/ui/fmtui/tdf89826.py b/sw/qa/uitest/ui/fmtui/tdf89826.py new file mode 100644 index 000000000000..d6f547779555 --- /dev/null +++ b/sw/qa/uitest/ui/fmtui/tdf89826.py @@ -0,0 +1,95 @@ +# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-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/. +# +"""Tests for tdf#89826 - interactive property chips on Organizer tab.""" + +from uitest.framework import UITestCase +from uitest.uihelper.common import select_pos +from uitest.uihelper.common import type_text +from libreoffice.uno.propertyvalue import mkPropertyValues + + +class tdf89826(UITestCase): + + def _find_and_click_first_chip(self, xDialog): + """Navigate into propbox and click the remove button on the first chip. + Returns True if a chip was found and clicked.""" + xPropBox = xDialog.getChild("propbox") + for sCatName in xPropBox.getChildren(): + xCatRow = xPropBox.getChild(sCatName) + if "chipsbox" not in xCatRow.getChildren(): + continue + xChipsBox = xCatRow.getChild("chipsbox") + for sRowName in xChipsBox.getChildren(): + xChipRow = xChipsBox.getChild(sRowName) + for sChipName in xChipRow.getChildren(): + xChip = xChipRow.getChild(sChipName) + if "removebar" in xChip.getChildren(): + xRemoveBar = xChip.getChild("removebar") + xRemoveBar.executeAction("CLICK", + mkPropertyValues({"POS": "0"})) + return True + return False + + def test_property_chip_reset(self): + """Test that removing a property via chip resets it to parent value, + and that undo restores it.""" + with self.ui_test.create_doc_in_start_center("writer"): + document = self.ui_test.get_component() + + # Create a new style + with self.ui_test.execute_dialog_through_command( + ".uno:StyleNewByExample") as xDialog: + xStyleName = xDialog.getChild("stylename") + type_text(xStyleName, "Test Chip Style") + + # Set parent and change font size in one dialog opening + with self.ui_test.execute_dialog_through_command( + ".uno:EditStyle") as xDialog: + xTabs = xDialog.getChild("tabcontrol") + # Tab 0 = Organizer: set parent + select_pos(xTabs, "0") + xLinkedWith = xTabs.getChild("linkedwith") + xLinkedWith.executeAction("SELECT", + mkPropertyValues({"TEXT": "Default Paragraph Style"})) + # Tab 1 = Character: change font size + select_pos(xTabs, "1") + xSizeWest = xTabs.getChild("cbWestSize") + xSizeWest.executeAction("CLEAR", tuple()) + type_text(xSizeWest, "20pt") + + # Verify font size was applied + xParaStyles = document.StyleFamilies.ParagraphStyles + self.assertEqual( + xParaStyles.getByName("Test Chip Style").getPropertyValue( + "CharHeight"), 20) + + # Open style dialog, switch to Edit mode, remove first chip + with self.ui_test.execute_dialog_through_command( + ".uno:EditStyle") as xDialog: + xTabs = xDialog.getChild("tabcontrol") + select_pos(xTabs, "0") + xEditBtn = xDialog.getChild("editprops") + xEditBtn.executeAction("CLICK", tuple()) + self.assertTrue(self._find_and_click_first_chip(xDialog), + "No property chip found to remove") + + # Property should now be inherited from parent + xParent = xParaStyles.getByName("Standard") + self.assertEqual( + xParaStyles.getByName("Test Chip Style").getPropertyValue( + "CharHeight"), + xParent.getPropertyValue("CharHeight")) + + # Undo should restore the removed property + self.xUITest.executeCommand(".uno:Undo") + self.assertEqual( + xParaStyles.getByName("Test Chip Style").getPropertyValue( + "CharHeight"), 20) + +# vim: set shiftwidth=4 softtabstop=4 expandtab: diff --git a/sw/source/ui/dialog/swdlgfact.cxx b/sw/source/ui/dialog/swdlgfact.cxx index 50d7567486a0..3329399de2b7 100644 --- a/sw/source/ui/dialog/swdlgfact.cxx +++ b/sw/source/ui/dialog/swdlgfact.cxx @@ -94,6 +94,7 @@ #include <translatelangselect.hxx> #include <copyfielddlg.hxx> #include <SwGridTabPage.hxx> +#include <set> using namespace css::frame; using namespace css::uno; @@ -375,6 +376,7 @@ public: return this->m_pDlg->GetInputRanges(pItem); } void SetInputSet(const SfxItemSet* pInSet) override { this->m_pDlg->SetInputSet(pInSet); } + const std::set<sal_uInt16>& GetInvalidatedWhichIds() const override { return this->m_pDlg->GetInvalidatedWhichIds(); } // From class Window. void SetText(const OUString& rStr) override { this->m_pDlg->set_title(rStr); } }; diff --git a/sw/source/ui/fmtui/tmpdlg.cxx b/sw/source/ui/fmtui/tmpdlg.cxx index a7035e3edbb1..aaf212b27223 100644 --- a/sw/source/ui/fmtui/tmpdlg.cxx +++ b/sw/source/ui/fmtui/tmpdlg.cxx @@ -267,7 +267,7 @@ SwTemplateDlgController::SwTemplateDlgController(weld::Window* pParent, short SwTemplateDlgController::Ok() { - short nRet = SfxTabDialogController::Ok(); + short nRet = SfxStyleDialogController::Ok(); if( RET_OK == nRet ) { const SfxPoolItem *pOutItem, *pExItem; diff --git a/sw/source/uibase/app/docst.cxx b/sw/source/uibase/app/docst.cxx index 577337abffa7..93b5b9d63acb 100644 --- a/sw/source/uibase/app/docst.cxx +++ b/sw/source/uibase/app/docst.cxx @@ -20,6 +20,7 @@ #include <config_wasm_strip.h> #include <memory> +#include <set> #include <com/sun/star/style/XStyleFamiliesSupplier.hpp> #include <com/sun/star/beans/XPropertySet.hpp> @@ -653,6 +654,11 @@ IMPL_LINK_NOARG(ApplyStyle, ApplyHdl, LinkParamNone*, void) // reset indent attributes at paragraph style, if a list style // will be applied and no indent attributes will be applied. m_xTmp->SetItemSet( aSet, false, true ); + + // Reset properties that user explicitly removed via property chips + const std::set<sal_uInt16>& rInvalidated = m_pDlg->GetInvalidatedWhichIds(); + if (!rInvalidated.empty()) + m_xTmp->ResetItems(rInvalidated); } else { diff --git a/sw/source/uibase/app/docstyle.cxx b/sw/source/uibase/app/docstyle.cxx index 45d0d8ad3c67..159a933e6b85 100644 --- a/sw/source/uibase/app/docstyle.cxx +++ b/sw/source/uibase/app/docstyle.cxx @@ -69,6 +69,7 @@ #include <svx/xfillit0.hxx> #include <svx/xflftrit.hxx> #include <svx/drawitem.hxx> +#include <svx/strarray.hxx> #include <names.hxx> using namespace com::sun::star; @@ -1203,6 +1204,275 @@ OUString SwDocStyleSheet::GetDescription(MapUnit eUnit) return SfxStyleSheetBase::GetDescription(eUnit); } +std::vector<std::pair<sal_uInt16, OUString>> SwDocStyleSheet::GetItemPresentation( + MapUnit eMetric, const SfxItemSet* pWorkingSet) +{ + std::vector<std::pair<sal_uInt16, OUString>> aResult; + IntlWrapper aIntlWrapper(SvtSysLocale().GetUILanguageTag()); + + // Get parent item set for comparison + const SfxItemSet* pParentSet = nullptr; + SfxStyleSheetBase* pParentStyle = nullptr; + if (!GetParent().isEmpty()) + { + pParentStyle = m_pPool->Find(GetParent(), nFamily); + if (pParentStyle) + pParentSet = &pParentStyle->GetItemSet(); + } + + if (SfxStyleFamily::Page == nFamily) + { + if (!pSet) + GetItemSet(); + + SfxItemIter aIter(*pSet); + + for (const SfxPoolItem* pItem = aIter.GetCurItem(); pItem; pItem = aIter.NextItem()) + { + if (!IsInvalidItem(pItem)) + { + // Only show items that are explicitly SET in this style, not inherited from parent + if (pSet->GetItemState(pItem->Which(), false) != SfxItemState::SET) + continue; + + // Skip items identical to parent + if (pParentSet) + { + const SfxPoolItem* pParentItem = nullptr; + if (pParentSet->GetItemState(pItem->Which(), true, &pParentItem) == SfxItemState::SET + && pParentItem && *pParentItem == *pItem) + continue; + } + + // Skip items whose value equals the pool default + if (*pItem == pSet->GetPool()->GetUserOrPoolDefaultItem(pItem->Which())) + continue; + + switch (pItem->Which()) + { + case RES_LR_SPACE: + case SID_ATTR_PAGE_SIZE: + case SID_ATTR_PAGE_MAXSIZE: + case SID_ATTR_PAGE_PAPERBIN: + case SID_ATTR_BORDER_INNER: + break; + default: + { + OUString aItemPresentation; + if (!IsInvalidItem(pItem) && + m_pPool->GetPool().GetPresentation( + *pItem, eMetric, aItemPresentation, aIntlWrapper)) + { + if (!aItemPresentation.isEmpty()) + { + // If there isn't "Name: value", try adding the name + if (aItemPresentation.indexOf(": ") == -1) + { + sal_uInt16 nSlotId = m_pPool->GetPool().GetSlotId(pItem->Which()); + sal_uInt32 nIdx = SvxAttrNameTable::FindIndex(nSlotId); + OUString aAttrName = SvxAttrNameTable::GetString(nIdx); + if (aAttrName.isEmpty()) + { + // Fallback for Writer-specific WhichId + static const std::map<sal_uInt16, TranslateId> aSwAttrNames = { + { sal_uInt16(RES_PARATR_OUTLINELEVEL), STR_ATTR_OUTLINE_LEVEL }, + { sal_uInt16(RES_PARATR_LIST_LEVEL), STR_ATTR_LIST_LEVEL }, + { sal_uInt16(RES_PARATR_LIST_ISRESTART), STR_ATTR_LIST_RESTART }, + { sal_uInt16(RES_PARATR_LIST_RESTARTVALUE), STR_ATTR_LIST_RESTART_VALUE }, + { sal_uInt16(RES_PARATR_AUTOFRAMEDIR), STR_ATTR_TEXT_DIRECTION }, + { sal_uInt16(RES_CHRATR_HIDDEN), STR_ATTR_HIDDEN }, + { sal_uInt16(XATTR_FILLSTYLE), STR_ATTR_FILL_STYLE }, + { sal_uInt16(XATTR_FILLGRADIENT), STR_ATTR_GRADIENT }, + { sal_uInt16(XATTR_GRADIENTSTEPCOUNT), STR_ATTR_GRADIENT_STEPS }, + }; + auto itW = aSwAttrNames.find(pItem->Which()); + if (itW != aSwAttrNames.end()) + aAttrName = SwResId(itW->second); + } + if (!aAttrName.isEmpty()) + aItemPresentation = aAttrName + ": " + aItemPresentation; + } + aResult.emplace_back(pItem->Which(), aItemPresentation); + } + } + } + } + } + } + return aResult; + } + + if (SfxStyleFamily::Frame == nFamily || SfxStyleFamily::Para == nFamily || SfxStyleFamily::Char == nFamily) + { + if (!pSet) + GetItemSet(); + + const SfxItemSet* pCheckSet = pWorkingSet ? pWorkingSet : pSet; + + const drawing::FillStyle eFillStyle(pCheckSet->Get(XATTR_FILLSTYLE).GetValue()); + const bool bUseFloatTransparence(pCheckSet->Get(XATTR_FILLFLOATTRANSPARENCE).IsEnabled()); + + SfxItemIter aIter(*pSet); + + for (const SfxPoolItem* pItem = aIter.GetCurItem(); pItem; pItem = aIter.NextItem()) + { + if (!IsInvalidItem(pItem)) + { + // Only show items that are explicitly SET in this style, not inherited from parent + if (pCheckSet->GetItemState(pItem->Which(), false) != SfxItemState::SET) + continue; + + // Skip items identical to parent + if (pParentSet) + { + const SfxPoolItem* pParentItem = nullptr; + if (pParentSet->GetItemState(pItem->Which(), true, &pParentItem) == SfxItemState::SET + && pParentItem && *pParentItem == *pItem) + continue; + } + + switch (pItem->Which()) + { + case SID_ATTR_AUTO_STYLE_UPDATE: + case RES_PAGEDESC: + case SID_ATTR_PARA_PAGENUM: + case SID_ATTR_PARA_MODEL: + case RES_BREAK: + // Skip these - they have special handling in GetDescription + break; + default: + { + OUString aItemPresentation; + if (!IsInvalidItem(pItem) && + m_pPool->GetPool().GetPresentation( + *pItem, eMetric, aItemPresentation, aIntlWrapper)) + { + bool bIsDefault = false; + switch (pItem->Which()) + { + case XATTR_FILLCOLOR: + bIsDefault = (drawing::FillStyle_SOLID == eFillStyle); + break; + case XATTR_FILLGRADIENT: + bIsDefault = (drawing::FillStyle_GRADIENT == eFillStyle); + break; + case XATTR_FILLHATCH: + bIsDefault = (drawing::FillStyle_HATCH == eFillStyle); + break; + case XATTR_FILLBITMAP: + bIsDefault = (drawing::FillStyle_BITMAP == eFillStyle); + break; + case XATTR_FILLTRANSPARENCE: + bIsDefault = !bUseFloatTransparence; + break; + case XATTR_FILLFLOATTRANSPARENCE: + bIsDefault = bUseFloatTransparence; + break; + case XATTR_GRADIENTSTEPCOUNT: + bIsDefault = bUseFloatTransparence; + break; + case RES_CHRATR_CJK_FONT: + case RES_CHRATR_CJK_FONTSIZE: + case RES_CHRATR_CJK_LANGUAGE: + case RES_CHRATR_CJK_POSTURE: + case RES_CHRATR_CJK_WEIGHT: + if (SvtCJKOptions::IsCJKFontEnabled()) + bIsDefault = true; + aItemPresentation = SwResId(STR_CJK_FONT) + aItemPresentation; + break; + case RES_CHRATR_CTL_FONT: + case RES_CHRATR_CTL_FONTSIZE: + case RES_CHRATR_CTL_LANGUAGE: + case RES_CHRATR_CTL_POSTURE: + case RES_CHRATR_CTL_WEIGHT: + if (SvtCTLOptions::IsCTLFontEnabled()) + bIsDefault = true; + aItemPresentation = SwResId(STR_CTL_FONT) + aItemPresentation; + break; + case RES_CHRATR_FONT: + case RES_CHRATR_FONTSIZE: + case RES_CHRATR_LANGUAGE: + case RES_CHRATR_POSTURE: + case RES_CHRATR_WEIGHT: + aItemPresentation = SwResId(STR_WESTERN_FONT) + aItemPresentation; + [[fallthrough]]; + default: + bIsDefault = true; + } + if (bIsDefault && !aItemPresentation.isEmpty()) + { + // If there isn't "Name: value", try adding the name + if (aItemPresentation.indexOf(": ") == -1) + { + sal_uInt16 nSlotId = m_pPool->GetPool().GetSlotId(pItem->Which()); + sal_uInt32 nIdx = SvxAttrNameTable::FindIndex(nSlotId); + OUString aAttrName = SvxAttrNameTable::GetString(nIdx); + if (aAttrName.isEmpty()) + { + // Fallback for Writer-specific WhichId + static const std::map<sal_uInt16, TranslateId> aSwAttrNames = { + { sal_uInt16(RES_PARATR_OUTLINELEVEL), STR_ATTR_OUTLINE_LEVEL }, + { sal_uInt16(RES_PARATR_LIST_LEVEL), STR_ATTR_LIST_LEVEL }, + { sal_uInt16(RES_PARATR_LIST_ISRESTART), STR_ATTR_LIST_RESTART }, + { sal_uInt16(RES_PARATR_LIST_RESTARTVALUE), STR_ATTR_LIST_RESTART_VALUE }, + { sal_uInt16(RES_PARATR_AUTOFRAMEDIR), STR_ATTR_TEXT_DIRECTION }, + { sal_uInt16(RES_CHRATR_HIDDEN), STR_ATTR_HIDDEN }, + { sal_uInt16(XATTR_FILLSTYLE), STR_ATTR_FILL_STYLE }, + { sal_uInt16(XATTR_FILLGRADIENT), STR_ATTR_GRADIENT }, + { sal_uInt16(XATTR_GRADIENTSTEPCOUNT), STR_ATTR_GRADIENT_STEPS }, + }; + auto itW = aSwAttrNames.find(pItem->Which()); + if (itW != aSwAttrNames.end()) + aAttrName = SwResId(itW->second); + } + if (!aAttrName.isEmpty()) + aItemPresentation = aAttrName + ": " + aItemPresentation; + } + aResult.emplace_back(pItem->Which(), aItemPresentation); + } + } + } + } + } + } + return aResult; + } + + if (SfxStyleFamily::Pseudo == nFamily) + { + return aResult; + } + + return SfxStyleSheetBase::GetItemPresentation(eMetric); +} + +void SwDocStyleSheet::ResetItems(const std::set<sal_uInt16>& rWhichIds) +{ + if (rWhichIds.empty()) + return; + + std::vector<sal_uInt16> aIds(rWhichIds.begin(), rWhichIds.end()); + + if (nFamily == SfxStyleFamily::Para) + { + SwTextFormatColl* pColl = m_rDoc.FindTextFormatCollByName(UIName(aName)); + if (pColl) + m_rDoc.ResetAttrAtFormat(aIds, *pColl); + } + else if (nFamily == SfxStyleFamily::Char) + { + SwCharFormat* pCharFormat = m_rDoc.FindCharFormatByName(UIName(aName)); + if (pCharFormat) + m_rDoc.ResetAttrAtFormat(aIds, *pCharFormat); + } + else if (nFamily == SfxStyleFamily::Frame) + { + SwFrameFormat* pFrameFormat = m_rDoc.FindFrameFormatByName(UIName(aName)); + if (pFrameFormat) + m_rDoc.ResetAttrAtFormat(aIds, *pFrameFormat); + } +} + // Set names bool SwDocStyleSheet::SetName(const OUString& rStr, bool bReindexNow) {
