From f2da01e3fce962a2e6443e98758e287d0c4309ef Mon Sep 17 00:00:00 2001 From: happymy <598176144@qq.com> Date: Sat, 17 Oct 2026 17:14:07 +0800 Subject: [PATCH 1/3] feat: add a pin button to each list row A clip can be pinned (never auto deleted) again by clicking the pin button that shows up on the hovered row. It uses the slot the never-auto-delete star has always been drawn in, so a pinned clip shows one solid star and the text does not move; the pin is drawn faded while it is only offered by the hover. Clicking it writes lDontAutoDelete through SetClipPinned, the symbol text of a row comes from the shared GetRowSymbols. --- src/GdiImageDrawer.cpp | 26 +++++- src/GdiImageDrawer.h | 6 +- src/QListCtrl.cpp | 154 ++++++++++++++++++++++++++------ src/QListCtrl.h | 10 +++ src/QPasteWnd.cpp | 193 +++++++++++++++++++++++++++++++---------- src/QPasteWnd.h | 6 ++ 6 files changed, 318 insertions(+), 77 deletions(-) diff --git a/src/GdiImageDrawer.cpp b/src/GdiImageDrawer.cpp index e12c8825..834d8915 100644 --- a/src/GdiImageDrawer.cpp +++ b/src/GdiImageDrawer.cpp @@ -95,7 +95,7 @@ void CGdiImageDrawer::Draw(CDC* pScreenDC, CDPI &dpi, CWnd *pWnd, CRect rc, bool Draw(pScreenDC, dpi, pWnd, x, y, mouseHover, mouseDown); } -void CGdiImageDrawer::Draw(CDC* pScreenDC, CDPI &dpi, CWnd *pWnd, int posX, int posY, bool mouseHover, bool mouseDown, int forceWidth, int forceHeight) +void CGdiImageDrawer::Draw(CDC* pScreenDC, CDPI &dpi, CWnd *pWnd, int posX, int posY, bool mouseHover, bool mouseDown, int forceWidth, int forceHeight, int opacity) { int width = m_pStdImage->m_pBitmap->GetWidth(); if (forceWidth != INT_MAX) @@ -135,7 +135,29 @@ void CGdiImageDrawer::Draw(CDC* pScreenDC, CDPI &dpi, CWnd *pWnd, int posX, int //ia.SetRemapTable(1, &blackToRed); Gdiplus::Graphics graphics(pScreenDC->m_hDC); - graphics.DrawImage(*m_pStdImage, posX, posY, width, height); + if (opacity >= 255) + { + graphics.DrawImage(*m_pStdImage, posX, posY, width, height); + } + else + { + // draw the png faded out (the hover only pin button) + if (opacity < 0) + opacity = 0; + + Gdiplus::ColorMatrix cm = { 0 }; + cm.m[0][0] = 1.0f; + cm.m[1][1] = 1.0f; + cm.m[2][2] = 1.0f; + cm.m[3][3] = (Gdiplus::REAL)opacity / 255.0f; + cm.m[4][4] = 1.0f; + + Gdiplus::ImageAttributes ia; + ia.SetColorMatrix(&cm); + + Gdiplus::RectF grect((Gdiplus::REAL)posX, (Gdiplus::REAL)posY, (Gdiplus::REAL)width, (Gdiplus::REAL)height); + graphics.DrawImage(*m_pStdImage, grect, 0, 0, (Gdiplus::REAL)width, (Gdiplus::REAL)height, Gdiplus::UnitPixel, &ia); + } //RectF grect; grect.X = posX, grect.Y = posY; grect.Width = width; grect.Height = height; //graphics.DrawImage(*m_pStdImage, grect, 0, 0, width, height, UnitPixel, &ia); diff --git a/src/GdiImageDrawer.h b/src/GdiImageDrawer.h index 6e5ebe6c..65cdb3be 100644 --- a/src/GdiImageDrawer.h +++ b/src/GdiImageDrawer.h @@ -11,12 +11,12 @@ class CGdiImageDrawer BOOL LoadStdImage(UINT id, LPCTSTR pType); BOOL LoadStdImageDPI(int dpi, UINT id96, UINT id120, UINT id144, UINT id168, UINT id192, LPCTSTR pType, UINT id225 = 0, UINT id250 = 0, UINT id275 = 0, UINT id300 = 0, UINT id325 = 0, UINT id350 = 0); - void Draw(CDC* pScreenDC, CDPI &dpi, CWnd *pWnd, int posX, int posY, bool mouseHover, bool mouseDown, int forceWidth = INT_MAX, int forceHeight = INT_MAX); + void Draw(CDC* pScreenDC, CDPI &dpi, CWnd *pWnd, int posX, int posY, bool mouseHover, bool mouseDown, int forceWidth = INT_MAX, int forceHeight = INT_MAX, int opacity = 255); void Draw(CDC* pScreenDC, CDPI &dpi, CWnd *pWnd, CRect rc, bool mouseHover, bool mouseDown); BOOL LoadRaw(unsigned char* bitmapData, int imageSize); - UINT ImageWidth() { return m_pStdImage->m_pBitmap->GetWidth(); } - UINT ImageHeight() { return m_pStdImage->m_pBitmap->GetHeight(); } + UINT ImageWidth() { return (m_pStdImage && m_pStdImage->m_pBitmap) ? m_pStdImage->m_pBitmap->GetWidth() : 0; } + UINT ImageHeight() { return (m_pStdImage && m_pStdImage->m_pBitmap) ? m_pStdImage->m_pBitmap->GetHeight() : 0; } void Reset(); diff --git a/src/QListCtrl.cpp b/src/QListCtrl.cpp index 3c9cf307..d3dd89a1 100644 --- a/src/QListCtrl.cpp +++ b/src/QListCtrl.cpp @@ -26,6 +26,9 @@ static char THIS_FILE[] = __FILE__; #define ROW_BOTTOM_BORDER 4 #define ROW_LEFT_BORDER 3 +// opacity of the pin button while it is only shown because the row is hovered. +// keep it far below full so a hovered clip is clearly not a pinned one +#define PIN_BUTTON_HOVER_ALPHA 60 #define COLOR_SHADOW RGB(245, 245, 245) #define DUMMY_COL_WIDTH 2 @@ -209,6 +212,9 @@ CQListCtrl::CQListCtrl() m_pFormatter = NULL; m_allSelected = false; m_rowHeight = 50; + m_hoverRow = -1; + m_pinButtonItem = -1; + m_trackingMouseLeave = FALSE; m_mouseOverScrollAreaStart = 0; m_showIfClipWasPasted = TRUE; m_bShowTextForFirstTenHotKeys = true; @@ -259,6 +265,7 @@ BEGIN_MESSAGE_MAP(CQListCtrl, CListCtrl) ON_NOTIFY_REFLECT(LVN_KEYDOWN, OnKeydown) ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomdrawList) ON_WM_MOUSEMOVE() + ON_WM_MOUSELEAVE() ON_WM_SYSKEYDOWN() ON_WM_ERASEBKGND() ON_WM_CREATE() @@ -427,6 +434,55 @@ void CQListCtrl::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) ::ReleaseDC(NULL, hDC); } +// the right end of the row area that really belongs to the list. The strip the +// list keeps for its scroll bar is inside the client rect but is not part of +// the list for the mouse, a click there goes to the window behind it +int CQListCtrl::GetVisibleRight() +{ + CRect rcListClient; + GetClientRect(&rcListClient); + + CRect rcWindow; + GetWindowRect(&rcWindow); + + int nExtra = rcWindow.Width() - rcListClient.Width(); + if (nExtra > 0) + rcListClient.right -= nExtra; + + return rcListClient.right; +} + +BOOL CQListCtrl::HitTestPinButton(CPoint point, int& nItem) +{ + int nRow = HitTest(point); + // a pin button only exists for the row the mouse is over, use the rect that + // row was last painted with + if (nRow < 0 || nRow != m_pinButtonItem) + return FALSE; + + CRect rcBtn = m_pinButtonRect; + rcBtn.InflateRect(m_windowDpi->Scale(3), m_windowDpi->Scale(2)); + if (rcBtn.PtInRect(point) == FALSE) + return FALSE; + + nItem = nRow; + + return TRUE; +} + +void CQListCtrl::OnMouseLeave() +{ + m_trackingMouseLeave = FALSE; + m_pinButtonItem = -1; + + if (m_hoverRow >= 0) + { + int nRow = m_hoverRow; + m_hoverRow = -1; + RefreshRow(nRow); + } +} + void CQListCtrl::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) { NMLVCUSTOMDRAW* pLVCD = reinterpret_cast(pNMHDR); @@ -555,37 +611,57 @@ void CQListCtrl::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) DrawBitMap(nItem, rcText, pDC, csText); // draw the symbol box - if (strSymbols.GetLength() > 0) + if (strSymbols.Find(_T("")) >= 0) //group { - if (strSymbols.Find(_T("")) >= 0) //group - { - m_groupFolder.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); - rcText.left += m_groupFolder.ImageWidth() + m_windowDpi->Scale(2); - } - if (strSymbols.Find(_T("")) >= 0) //don't auto delete - { - m_dontDeleteImage.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); - rcText.left += m_dontDeleteImage.ImageWidth() + m_windowDpi->Scale(2); - } - if (strSymbols.Find(_T("")) >= 0) // has shortcut - { - m_shortCutImage.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); - rcText.left += m_shortCutImage.ImageWidth() + m_windowDpi->Scale(2); - } - if (drawInGroupIcon && - strSymbols.Find(_T("")) >= 0) // in group - { - m_inFolderImage.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); - rcText.left += m_inFolderImage.ImageWidth() + m_windowDpi->Scale(2); - } - if (strSymbols.Find(_T("")) >= 0) // has quick paste text + m_groupFolder.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); + rcText.left += m_groupFolder.ImageWidth() + m_windowDpi->Scale(2); + } + + // the pin button is the "never auto delete" star, so it sits in the slot + // that star has always been drawn in and a pinned clip shows one star + // and not two. A pinned clip shows it solid, a hovered clip shows it + // faint so it can be pinned. The slot is kept free for every row so the + // text does not move when the star shows up. + int nStarWidth = m_dontDeleteImage.ImageWidth(); + if (nStarWidth > 0) + { + CRect rcStar(rcText.left, rcText.top, rcText.left + nStarWidth, + rcText.top + m_dontDeleteImage.ImageHeight()); + bool bPinned = (strSymbols.Find(_T("")) >= 0); + bool bHover = (nItem == m_hoverRow); + + // remember where the star of the hovered row is, the click on it is + // handled outside this paint + if (bHover) { + m_pinButtonRect = rcStar; + m_pinButtonItem = nItem; } - if (strSymbols.Find(_T("")) >= 0) //sticky clip + + if (bPinned || bHover) { - m_stickyImage.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); - rcText.left += m_stickyImage.ImageWidth() + m_windowDpi->Scale(2); + m_dontDeleteImage.Draw(pDC, *m_windowDpi, this, rcStar.left, rcStar.top, false, false, + rcStar.Width(), rcStar.Height(), bPinned ? 255 : PIN_BUTTON_HOVER_ALPHA); } + + rcText.left += nStarWidth + m_windowDpi->Scale(2); + } + + if (strSymbols.Find(_T("")) >= 0) // has shortcut + { + m_shortCutImage.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); + rcText.left += m_shortCutImage.ImageWidth() + m_windowDpi->Scale(2); + } + if (drawInGroupIcon && + strSymbols.Find(_T("")) >= 0) // in group + { + m_inFolderImage.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); + rcText.left += m_inFolderImage.ImageWidth() + m_windowDpi->Scale(2); + } + if (strSymbols.Find(_T("")) >= 0) //sticky clip + { + m_stickyImage.Draw(pDC, *m_windowDpi, this, rcText.left, rcText.top, false, false); + rcText.left += m_stickyImage.ImageWidth() + m_windowDpi->Scale(2); } if (DrawRtfText(nItem, rcText, pDC) == FALSE) @@ -2100,6 +2176,32 @@ BOOL CQListCtrl::OnItemDeleted(long lID) void CQListCtrl::OnMouseMove(UINT nFlags, CPoint point) { + // highlight the row under the mouse so its pin button shows up + int nHover = HitTest(point); + if (nHover != m_hoverRow) + { + int nPrev = m_hoverRow; + m_hoverRow = nHover; + // the star of the previous row is not a button any more + m_pinButtonItem = -1; + + if (nPrev >= 0) + RefreshRow(nPrev); + if (nHover >= 0) + RefreshRow(nHover); + } + + if (m_trackingMouseLeave == FALSE) + { + TRACKMOUSEEVENT tme = { 0 }; + tme.cbSize = sizeof(TRACKMOUSEEVENT); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = m_hWnd; + + if (::TrackMouseEvent(&tme)) + m_trackingMouseLeave = TRUE; + } + if (CGetSetOptions::m_showScrollBar == FALSE) { CRect crWindow; diff --git a/src/QListCtrl.h b/src/QListCtrl.h index 01ef076d..55bfebd7 100644 --- a/src/QListCtrl.h +++ b/src/QListCtrl.h @@ -154,6 +154,8 @@ class CQListCtrl : public CListCtrl int GetRowHeight() { return m_rowHeight; } + BOOL HitTestPinButton(CPoint point, int& nItem); + void SetSearchText(CString text); void SetDpiInfo(CDPI *dpi); @@ -195,6 +197,13 @@ class CQListCtrl : public CListCtrl CGdiImageDrawer m_shortCutImage; CGdiImageDrawer m_stickyImage; int m_rowHeight; + int m_hoverRow; + BOOL m_trackingMouseLeave; + // rect of the pin star of the hovered row, kept up to date while that row + // is painted + CRect m_pinButtonRect; + int m_pinButtonItem; + int GetVisibleRight(); CString m_searchText; BOOL m_showIfClipWasPasted; CAccels *m_pToolTipActions; @@ -219,6 +228,7 @@ class CQListCtrl : public CListCtrl afx_msg BOOL OnToolTipText(UINT id, NMHDR * pNMHDR, LRESULT * pResult); afx_msg void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct); afx_msg void OnMouseMove(UINT nFlags, CPoint point); + afx_msg void OnMouseLeave(); DECLARE_MESSAGE_MAP() public: afx_msg void OnKillFocus(CWnd* pNewWnd); diff --git a/src/QPasteWnd.cpp b/src/QPasteWnd.cpp index 08fedfc6..9582b963 100644 --- a/src/QPasteWnd.cpp +++ b/src/QPasteWnd.cpp @@ -87,6 +87,7 @@ CQPasteWnd::CQPasteWnd() m_lastDbWrite = 0; m_pendingRefresh = false; m_lastNonActiveMouseMove = 0; + m_bSwallowPinUp = FALSE; } CQPasteWnd::~CQPasteWnd() @@ -2396,6 +2397,126 @@ void CQPasteWnd::OnMenuQuickpropertiesAutodelete() m_lstHeader.RefreshVisibleRows(); } +// the symbol part of a row, "..." - the same text GetDispInfo +// puts in front of the clip text +CString CQPasteWnd::GetRowSymbols(int iItem) +{ + CString cs; + + if (m_listItems[iItem].m_bDontAutoDelete) + { + cs += _T(""); + } + + if (m_listItems[iItem].m_bHasShortCut) + { + cs += _T(""); + } + + if (m_listItems[iItem].m_bIsGroup) + { + cs += _T(""); + } + + if (theApp.m_GroupID > 0) + { + if (m_listItems[iItem].m_stickyClipGroupOrder != INVALID_STICKY) + { + cs += _T(""); + } + } + else + { + if (m_listItems[iItem].m_stickyClipOrder != INVALID_STICKY) + { + cs += _T(""); + } + } + + // attached to a group + if (m_listItems[iItem].m_bHasParent) + { + cs += _T(""); + } + + if (m_listItems[iItem].m_QuickPaste.IsEmpty() == FALSE) + { + cs += _T(""); + } + + if (m_listItems[iItem].m_dateCopied != m_listItems[iItem].m_datePasted) + { + cs += ""; + } + + return cs; +} + +// the pin button of a row was clicked, toggle it +void CQPasteWnd::TogglePin(int nRow) +{ + bool bPinned = false; + + { + ATL::CCritSecLock csLock(m_CritSection.m_sect); + + if (nRow < 0 || nRow >= (int)m_listItems.size()) + { + return; + } + + bPinned = m_listItems[nRow].m_bDontAutoDelete; + } + + SetClipPinned(nRow, bPinned == false); +} + +// set or clear the "never auto delete" flag of a single row (the pin button) +void CQPasteWnd::SetClipPinned(int nRow, bool bPinned) +{ + int nID = -1; + + { + ATL::CCritSecLock csLock(m_CritSection.m_sect); + + if (nRow < 0 || nRow >= (int)m_listItems.size()) + { + return; + } + + nID = m_listItems[nRow].m_lID; + } + + if (nID <= 0) + { + return; + } + + try + { + if (bPinned) + { + theApp.m_db.execDMLEx(_T("UPDATE Main SET lDontAutoDelete = %d where lID = %d;"), (int)CTime::GetCurrentTime().GetTime(), nID); + } + else + { + theApp.m_db.execDMLEx(_T("UPDATE Main SET lDontAutoDelete = 0 where lID = %d;"), nID); + } + } + CATCH_SQLITE_EXCEPTION + + { + ATL::CCritSecLock csLock(m_CritSection.m_sect); + + if (nRow < (int)m_listItems.size()) + { + m_listItems[nRow].m_bDontAutoDelete = bPinned; + } + } + + m_lstHeader.RefreshRow(nRow); +} + void CQPasteWnd::OnMenuQuickpropertiesRemovehotkey() { CWaitCursor wait; @@ -3045,6 +3166,31 @@ void CQPasteWnd::SetKeyModiferState(bool bActive) BOOL CQPasteWnd::PreTranslateMessage(MSG* pMsg) { + if (pMsg->message == WM_LBUTTONDOWN || pMsg->message == WM_LBUTTONUP) + { + // a click on the pin button of a row pins/unpins the row, the action + // that is configured for clicking a clip must not run for it + if (pMsg->message == WM_LBUTTONDOWN && pMsg->hwnd == m_lstHeader.m_hWnd) + { + CPoint point(pMsg->pt); + m_lstHeader.ScreenToClient(&point); + + int nPinRow = -1; + if (m_lstHeader.HitTestPinButton(point, nPinRow)) + { + TogglePin(nPinRow); + m_bSwallowPinUp = TRUE; + return TRUE; + } + } + else if (pMsg->message == WM_LBUTTONUP && m_bSwallowPinUp) + { + // the button up that belongs to the swallowed pin down + m_bSwallowPinUp = FALSE; + return TRUE; + } + } + switch (pMsg->message) { case WM_MBUTTONUP: @@ -5690,52 +5836,7 @@ void CQPasteWnd::GetDispInfo(NMHDR* pNMHDR, LRESULT* pResult) if ((int)m_listItems.size() > pItem->iItem && m_listItems[pItem->iItem].m_lID > 0) { - CString cs; - if (m_listItems[pItem->iItem].m_bDontAutoDelete) - { - cs += _T(""); - } - - if (m_listItems[pItem->iItem].m_bHasShortCut) - { - cs += _T(""); - } - - if (m_listItems[pItem->iItem].m_bIsGroup) - { - cs += _T(""); - } - - if (theApp.m_GroupID > 0) - { - if (m_listItems[pItem->iItem].m_stickyClipGroupOrder != INVALID_STICKY) - { - cs += _T(""); - } - } - else - { - if (m_listItems[pItem->iItem].m_stickyClipOrder != INVALID_STICKY) - { - cs += _T(""); - } - } - - // attached to a group - if (m_listItems[pItem->iItem].m_bHasParent) - { - cs += _T(""); - } - - if (m_listItems[pItem->iItem].m_QuickPaste.IsEmpty() == FALSE) - { - cs += _T(""); - } - - if (m_listItems[pItem->iItem].m_dateCopied != m_listItems[pItem->iItem].m_datePasted) - { - cs += ""; - } + CString cs = GetRowSymbols(pItem->iItem); // pipe is the "end of symbols" marker cs += "|" + CMainTableFunctions::GetDisplayText(CGetSetOptions::m_nLinesPerRow, m_listItems[pItem->iItem].m_Desc); diff --git a/src/QPasteWnd.h b/src/QPasteWnd.h index 2a42b5ec..48dfb96c 100644 --- a/src/QPasteWnd.h +++ b/src/QPasteWnd.h @@ -178,6 +178,7 @@ class CQPasteWnd: public CWndEx __int64 m_lastDbWrite; bool m_pendingRefresh; DWORD m_lastNonActiveMouseMove; + bool m_bSwallowPinUp; void RefreshNc(); void UpdateStatus(bool bRepaintImmediately = false); // regenerates the status (caption) text @@ -347,6 +348,11 @@ class CQPasteWnd: public CWndEx void SetCurrentTransparency(); + // pin (never auto delete) helpers + CString GetRowSymbols(int iItem); + void SetClipPinned(int nRow, bool bPinned); + void TogglePin(int nRow); + // Generated message map functions protected: //{{AFX_MSG(CQPasteWnd) From 7457d4358528e5d3eec9ce21fae81f24bdc4cbc2 Mon Sep 17 00:00:00 2001 From: happymy <598176144@qq.com> Date: Sat, 17 Oct 2026 17:14:21 +0800 Subject: [PATCH 2/3] feat: wrap long clip text inside a row A row has one height for every clip, so text longer than a single line was cut off at the right. The text now wraps to fill the lines the row height covers and is clipped at the bottom; a clip that needs fewer lines stays vertically centered like before. The text is also kept inside the part of the row that is really visible, the strip the list holds for its scroll bar is not part of it. --- src/QListCtrl.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/QListCtrl.cpp b/src/QListCtrl.cpp index d3dd89a1..39f086be 100644 --- a/src/QListCtrl.cpp +++ b/src/QListCtrl.cpp @@ -427,6 +427,9 @@ void CQListCtrl::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) GetTextMetrics(hDC, &tm); if (m_windowDpi != NULL) { + // windows list views only support one height for all rows, every row gets + // the room of "lines per clip" lines and the wrapped text is drawn into + // it. a clip needing more lines is cut off at the bottom of the row. lpMeasureItemStruct->itemHeight = ((tm.tmHeight + tm.tmExternalLeading) * m_linesPerRow) + m_windowDpi->Scale(ROW_BOTTOM_BORDER); m_rowHeight = lpMeasureItemStruct->itemHeight; } @@ -571,6 +574,10 @@ void CQListCtrl::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) rcText.left += m_windowDpi->Scale(ROW_LEFT_BORDER); rcText.top += m_windowDpi->Scale(1); rcText.bottom -= m_windowDpi->Scale(1); + // a row can be wider than the control, keep the text inside the part of + // the row that is really visible + if (rcText.right > GetVisibleRight()) + rcText.right = GetVisibleRight(); if (m_showIfClipWasPasted && strSymbols.GetLength() > 0 && @@ -671,11 +678,21 @@ void CQListCtrl::OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult) if (m_searchText.GetLength() > 0 && FindNoCaseAndInsert(csText, m_searchText, StrF(_T("\x01\x04 color='#%02x%02x%02x'\x02"), GetRValue(highlightColor), GetGValue(highlightColor), GetBValue(highlightColor)), _T("\x01\x03\x04\x02"), m_linesPerRow) > 0) { - DrawHTML(pDC->m_hDC, csText, csText.GetLength(), rcText, DT_VCENTER | DT_EXPANDTABS | DT_NOPREFIX); + DrawHTML(pDC->m_hDC, csText, csText.GetLength(), rcText, DT_WORDBREAK | DT_EXPANDTABS | DT_NOPREFIX); } else { - pDC->DrawText(csText, rcText, DT_VCENTER | DT_EXPANDTABS | DT_NOPREFIX); + // keep a clip that needs less room than the row has vertically + // centered, like it was before the text could wrap + CRect rcMeasure = rcText; + int nTextHeight = pDC->DrawText(csText, rcMeasure, DT_CALCRECT | DT_WORDBREAK | DT_EXPANDTABS | DT_NOPREFIX); + if (nTextHeight > 0 && nTextHeight < rcText.Height()) + { + rcText.top += (rcText.Height() - nTextHeight) / 2; + rcText.bottom = rcText.top + nTextHeight; + } + + pDC->DrawText(csText, rcText, DT_WORDBREAK | DT_EXPANDTABS | DT_NOPREFIX); } } From 7ac01629efb9a211b47f7eb8da247ccc2dcf2932 Mon Sep 17 00:00:00 2001 From: happymy <598176144@qq.com> Date: Sat, 17 Oct 2026 17:15:30 +0800 Subject: [PATCH 3/3] feat: warn in red on the delete-all-clips menu entry The entry that deletes every clip that is not pinned looked like the harmless entries next to it. Windows draws its menus itself and cannot color a single entry, so this one is switched to owner draw and painted by the app with the menu font and the system colors windows uses for the entries around it. The text still comes from the language file. A menu that holds an owner drawn entry is drawn by Windows the classic way, so this menu no longer follows the modern style. --- src/QPasteWnd.cpp | 118 ++++++++++++++++++++++++++++++++++++++++++++++ src/QPasteWnd.h | 3 ++ 2 files changed, 121 insertions(+) diff --git a/src/QPasteWnd.cpp b/src/QPasteWnd.cpp index 9582b963..662d297b 100644 --- a/src/QPasteWnd.cpp +++ b/src/QPasteWnd.cpp @@ -49,6 +49,16 @@ static char THIS_FILE[] = __FILE__; #define ID_SYSTEM_BUTTON 0x210 #define ID_NO_SEARCH_RESULTS 0x211 +// The entry of the system menu that deletes clips is drawn in red to warn +// about it. Windows draws its menus itself and cannot color a single entry, +// so this entry is switched to owner draw and painted in OnDrawItem with the +// size and the colors windows uses for the entries around it (a menu that +// holds an owner drawn entry is drawn by windows the classic way). +#define MENU_DANGER_TEXT_COLOR RGB(200, 0, 0) +#define MENU_ITEM_HEIGHT 17 +#define MENU_ITEM_TEXT_LEFT 17 +#define MENU_ITEM_RIGHT_MARGIN 20 + #define QPASTE_WIDTH 200 #define QPASTE_HEIGHT 200 @@ -209,6 +219,8 @@ BEGIN_MESSAGE_MAP(CQPasteWnd, CWndEx) ON_MESSAGE(NM_GROUP_TREE_MESSAGE, OnGroupTreeMessage) ON_COMMAND(ID_BACK_BUTTON, OnBackButton) ON_COMMAND(ID_SYSTEM_BUTTON, OnSystemButton) + ON_WM_DRAWITEM() + ON_WM_MEASUREITEM() ON_MESSAGE(CB_UPDOWN, OnUpDown) ON_MESSAGE(NM_INACTIVE_TOOLTIPWND, OnToolTipWndInactive) ON_MESSAGE(NM_SET_LIST_COUNT, OnSetListCount) @@ -7379,10 +7391,116 @@ void CQPasteWnd::OnSystemButton() SetMenuChecks(cmSubMenu); + // the entry that throws clips away for good is drawn in red. windows + // draws menus itself and cannot color one entry, so that entry is made + // owner draw and painted in OnDrawItem/OnMeasureItem + int nDeleteAllPos = -1; + for (int i = 0; i < ::GetMenuItemCount(cmSubMenu->GetSafeHmenu()); i++) + { + if (::GetMenuItemID(cmSubMenu->GetSafeHmenu(), i) == ID_MENU_DELETEALLNONUSEDCLIPS) + { + nDeleteAllPos = i; + break; + } + } + + if (nDeleteAllPos >= 0) + { + TCHAR szDeleteAllText[128] = { 0 }; + ::GetMenuString(cmSubMenu->GetSafeHmenu(), nDeleteAllPos, szDeleteAllText, _countof(szDeleteAllText), MF_BYPOSITION); + m_strDeleteAllClipsText = szDeleteAllText; + + MENUITEMINFO mii = { 0 }; + mii.cbSize = sizeof(MENUITEMINFO); + mii.fMask = MIIM_FTYPE | MIIM_DATA; + mii.fType = MFT_OWNERDRAW; + mii.dwItemData = ID_MENU_DELETEALLNONUSEDCLIPS; + ::SetMenuItemInfo(cmSubMenu->GetSafeHmenu(), nDeleteAllPos, TRUE, &mii); + } + cmSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON, pp.x, pp.y, this, NULL); } } +void CQPasteWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) +{ + if (lpMeasureItemStruct->CtlType == ODT_MENU && + lpMeasureItemStruct->itemID == ID_MENU_DELETEALLNONUSEDCLIPS) + { + // keep the size of the entries windows draws itself. the text is + // translated, so the width comes from the text and not from a constant + lpMeasureItemStruct->itemHeight = m_DittoWindow.m_dpi.Scale(MENU_ITEM_HEIGHT); + lpMeasureItemStruct->itemWidth = m_DittoWindow.m_dpi.Scale(MENU_ITEM_TEXT_LEFT + MENU_ITEM_RIGHT_MARGIN); + + NONCLIENTMETRICS ncm; + memset(&ncm, 0, sizeof(ncm)); + ncm.cbSize = sizeof(ncm); + if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) + { + CDC* pDC = GetDC(); + if (pDC != NULL) + { + CFont font; + font.CreateFontIndirect(&ncm.lfMenuFont); + CFont* pOldFont = pDC->SelectObject(&font); + lpMeasureItemStruct->itemWidth += pDC->GetTextExtent(m_strDeleteAllClipsText).cx; + pDC->SelectObject(pOldFont); + ReleaseDC(pDC); + } + } + return; + } + + CWnd::OnMeasureItem(nIDCtl, lpMeasureItemStruct); +} + +void CQPasteWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) +{ + if (lpDrawItemStruct->CtlType != ODT_MENU || + (lpDrawItemStruct->itemID != ID_MENU_DELETEALLNONUSEDCLIPS && + lpDrawItemStruct->itemData != ID_MENU_DELETEALLNONUSEDCLIPS)) + { + CWnd::OnDrawItem(nIDCtl, lpDrawItemStruct); + return; + } + + CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); + CRect rcItem(lpDrawItemStruct->rcItem); + const bool bSelected = (lpDrawItemStruct->itemState & ODS_SELECTED) != 0; + const bool bDisabled = (lpDrawItemStruct->itemState & (ODS_DISABLED | ODS_GRAYED)) != 0; + + pDC->FillSolidRect(rcItem, bSelected ? ::GetSysColor(COLOR_HIGHLIGHT) : ::GetSysColor(COLOR_MENU)); + + // the font windows uses for its own menu entries + NONCLIENTMETRICS ncm; + memset(&ncm, 0, sizeof(ncm)); + ncm.cbSize = sizeof(ncm); + if (!SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) + { + return; + } + + CFont font; + font.CreateFontIndirect(&ncm.lfMenuFont); + CFont* pOldFont = pDC->SelectObject(&font); + + CRect rcText(rcItem); + rcText.left += m_DittoWindow.m_dpi.Scale(MENU_ITEM_TEXT_LEFT); + + pDC->SetBkMode(TRANSPARENT); + if (bDisabled) + { + pDC->SetTextColor(::GetSysColor(COLOR_GRAYTEXT)); + } + else + { + pDC->SetTextColor(bSelected ? ::GetSysColor(COLOR_HIGHLIGHTTEXT) : MENU_DANGER_TEXT_COLOR); + } + pDC->DrawText(m_strDeleteAllClipsText, &rcText, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX); + + pDC->SelectObject(pOldFont); +} + void CQPasteWnd::OnSpecialpasteRemovelinefeeds() { DoAction(ActionEnums::PASTE_REMOVE_LINE_FEEDS); diff --git a/src/QPasteWnd.h b/src/QPasteWnd.h index 48dfb96c..af3696d6 100644 --- a/src/QPasteWnd.h +++ b/src/QPasteWnd.h @@ -179,6 +179,7 @@ class CQPasteWnd: public CWndEx bool m_pendingRefresh; DWORD m_lastNonActiveMouseMove; bool m_bSwallowPinUp; + CString m_strDeleteAllClipsText; void RefreshNc(); void UpdateStatus(bool bRepaintImmediately = false); // regenerates the status (caption) text @@ -466,6 +467,8 @@ class CQPasteWnd: public CWndEx afx_msg void OnMenuNewGroupSelection(); afx_msg void OnBackButton(); afx_msg void OnSystemButton(); + afx_msg void OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct); + afx_msg void OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct); afx_msg LRESULT OnUpDown(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnItemDeleted(WPARAM wParam, LPARAM lParam); LRESULT OnToolTipWndInactive(WPARAM wParam, LPARAM lParam);