diff --git a/CP_Main.rc b/CP_Main.rc index 5a41e2b4..c51e081d 100644 --- a/CP_Main.rc +++ b/CP_Main.rc @@ -671,6 +671,17 @@ BEGIN MENUITEM SEPARATOR MENUITEM "Show Thumbnails", 32845 MENUITEM "Draw RTF Text", 32846 + MENUITEM SEPARATOR + MENUITEM "Show hover preview", ID_MENU_HOVER_PREVIEW_TOGGLE + POPUP "Preview window size" + BEGIN + MENUITEM "Original Size", ID_MENU_HOVER_PREVIEW_ORIGINAL + MENUITEM "200 Pixels", ID_MENU_HOVER_PREVIEW_200 + MENUITEM "300 Pixels", ID_MENU_HOVER_PREVIEW_300 + MENUITEM "400 Pixels", ID_MENU_HOVER_PREVIEW_400 + MENUITEM "600 Pixels", ID_MENU_HOVER_PREVIEW_600 + MENUITEM "800 Pixels", ID_MENU_HOVER_PREVIEW_800 + END END MENUITEM SEPARATOR MENUITEM "Show Startup Message", ID_FIRST_SHOWSTARTUPMESSAGE diff --git a/CP_Main.vcxproj b/CP_Main.vcxproj index cd39b313..5243e04e 100644 --- a/CP_Main.vcxproj +++ b/CP_Main.vcxproj @@ -595,6 +595,7 @@ + @@ -860,6 +861,7 @@ + diff --git a/CP_Main.vcxproj.filters b/CP_Main.vcxproj.filters index ba9944ba..7fa3a40c 100644 --- a/CP_Main.vcxproj.filters +++ b/CP_Main.vcxproj.filters @@ -178,6 +178,9 @@ src + + src + src @@ -685,6 +688,9 @@ src + + src + src diff --git a/Debug/Language/Chinese Simplified.xml b/Debug/Language/Chinese Simplified.xml index 406de259..91f327cb 100644 --- a/Debug/Language/Chinese Simplified.xml +++ b/Debug/Language/Chinese Simplified.xml @@ -55,6 +55,14 @@ 发送到 (192.168.1.101) - 同一台计算机 查看完整说明 + 显示悬停预览 + 预览窗口大小 + 原图尺寸 + 200 像素 + 300 像素 + 400 像素 + 600 像素 + 800 像素 查看组 仅粘贴纯文本 将 HTML 粘贴为纯文本 diff --git a/Debug/Language/English.xml b/Debug/Language/English.xml index 9d87d6e3..b6e1427a 100644 --- a/Debug/Language/English.xml +++ b/Debug/Language/English.xml @@ -48,6 +48,14 @@ + Show hover preview + Preview window size + Original Size + 200 Pixels + 300 Pixels + 400 Pixels + 600 Pixels + 800 Pixels diff --git a/resource.h b/resource.h index 6ecab036..1a625204 100644 --- a/resource.h +++ b/resource.h @@ -871,7 +871,19 @@ #define BUTTON_BULLET 32995 #define ID_BUTTONWRAP 32996 #define ID_IMPORT_EXPORTTOWEBSEARCH 32997 -#define ID_SPECIALPASTE_PASTENEWGUID 32998 +# +// Hover preview popup of the quick paste list. The ids start past the range +// the preview pane work uses (33003 - 33006) so that the two do not clash. +#define ID_MENU_HOVER_PREVIEW_TOGGLE 33020 +#define ID_MENU_HOVER_PREVIEW_ORIGINAL 33021 +#define ID_MENU_HOVER_PREVIEW_200 33022 +#define ID_MENU_HOVER_PREVIEW_300 33023 +#define ID_MENU_HOVER_PREVIEW_400 33024 +#define ID_MENU_HOVER_PREVIEW_600 33025 +#define ID_MENU_HOVER_PREVIEW_800 33026 +#define ID_MENU_HOVER_PREVIEW_FIRST ID_MENU_HOVER_PREVIEW_ORIGINAL +#define ID_MENU_HOVER_PREVIEW_LAST ID_MENU_HOVER_PREVIEW_800 +define ID_SPECIALPASTE_PASTENEWGUID 32998 #define ID_SPECIALPASTE_POSIXIFY_PATHS 32999 #define ID_MENU_SHOWSTARREDCLIPS 33000 #define ID_SPECIALPASTE_PASTEASIMAGE 33001 @@ -883,7 +895,7 @@ #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_3D_CONTROLS 1 #define _APS_NEXT_RESOURCE_VALUE 394 -#define _APS_NEXT_COMMAND_VALUE 33002 +#define _APS_NEXT_COMMAND_VALUE 33027 #define _APS_NEXT_CONTROL_VALUE 2174 #define _APS_NEXT_SYMED_VALUE 104 #endif diff --git a/src/HoverPreviewWnd.cpp b/src/HoverPreviewWnd.cpp new file mode 100644 index 00000000..51dae3a6 --- /dev/null +++ b/src/HoverPreviewWnd.cpp @@ -0,0 +1,1309 @@ +// HoverPreviewWnd.cpp : implementation file + +#include "stdafx.h" +#include "HoverPreviewWnd.h" +#include "CP_Main.h" +#include "Clip.h" +#include "Misc.h" +#include "FormattedTextDraw.h" +#include "RichEditCtrlEx.h" +#include "ImageHelper.h" +#include + +#ifdef _DEBUG +#define new DEBUG_NEW +#undef THIS_FILE +static char THIS_FILE[] = __FILE__; +#endif + +// Distance between the list row and the popup +#define HOVER_PREVIEW_POPUP_GAP 8 +// Border and inner padding of the popup +#define HOVER_PREVIEW_MARGIN 4 +// Gap between the preview and the information text +#define HOVER_PREVIEW_TEXT_GAP 4 +// Smallest width of the popup, so that the information text stays readable +#define HOVER_PREVIEW_MIN_WIDTH 160 +// The information text shows at least this many lines, the configured tool +// tip line count wins when it is bigger +#define HOVER_PREVIEW_MIN_TEXT_LINES 6 +// Slack added to a measured text width so that it does not wrap +#define HOVER_PREVIEW_WIDTH_SLACK 8 +// Width used for the previews that have no size of their own (rtf, colour, +// html) when the menu is on "Original Size" +#define HOVER_PREVIEW_DEFAULT_WIDTH 400 +// A colour swatch is half as high as it is wide, within these limits +#define HOVER_PREVIEW_SWATCH_MIN 48 +#define HOVER_PREVIEW_SWATCH_MAX 180 + +CHoverPreviewWnd* CHoverPreviewWnd::s_pInstance = NULL; + +CHoverPreviewWnd::CHoverPreviewWnd() +{ + m_kind = KIND_NONE; + m_pBitmap = NULL; + m_pRtfDraw = NULL; + m_pRtfNormalizer = NULL; + m_crColor = 0; + m_nColorAlpha = 255; + m_lClipId = -1; +} + +CHoverPreviewWnd::~CHoverPreviewWnd() +{ + if (m_pBitmap != NULL) + { + delete m_pBitmap; + m_pBitmap = NULL; + } + + if (m_pRtfDraw != NULL) + { + delete m_pRtfDraw; + m_pRtfDraw = NULL; + } +} + +BEGIN_MESSAGE_MAP(CHoverPreviewWnd, CWnd) + ON_WM_PAINT() + ON_WM_ERASEBKGND() +END_MESSAGE_MAP() + +CHoverPreviewWnd* CHoverPreviewWnd::Get() +{ + if (s_pInstance == NULL) + { + s_pInstance = new CHoverPreviewWnd(); + } + + return s_pInstance; +} + +bool CHoverPreviewWnd::CreateIfNeeded(CWnd* pOwner) +{ + if (::IsWindow(GetSafeHwnd())) + { + return true; + } + + if (pOwner == NULL || ::IsWindow(pOwner->GetSafeHwnd()) == FALSE) + { + return false; + } + + // CS_SAVEBITS makes hiding and showing the popup cheap, a NULL background + LPCTSTR lpszClass = AfxRegisterWndClass(CS_SAVEBITS, ::LoadCursor(NULL, IDC_ARROW), NULL, NULL); + + // WS_EX_NOACTIVATE is required: the quick paste window hides itself when + // it loses the activation, so this popup must never be activated. + // WS_EX_TOOLWINDOW keeps it out of the task bar and out of alt-tab. + if (CWnd::CreateEx(WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, + lpszClass, + _T(""), + WS_POPUP, + 0, 0, 0, 0, + pOwner->GetSafeHwnd(), + NULL, + NULL) == FALSE) + { + return false; + } + + m_dpi.SetHwnd(GetSafeHwnd()); + + return true; +} + +bool CHoverPreviewWnd::LoadImage(long lClipId) +{ + CClipFormat Clip; + + Clip.m_cfType = CF_DIB; + theApp.GetClipData(lClipId, Clip); + + if (Clip.m_hgData == NULL) + { + Clip.m_cfType = theApp.m_PNG_Format; + theApp.GetClipData(lClipId, Clip); + } + + Gdiplus::Bitmap* pBitmap = NULL; + if (Clip.m_hgData != NULL) + { + pBitmap = Clip.CreateGdiplusBitmap(); + } + + Clip.Free(); + Clip.Clear(); + + if (pBitmap == NULL || + pBitmap->GetWidth() == 0 || + pBitmap->GetHeight() == 0) + { + if (pBitmap != NULL) + { + delete pBitmap; + } + + return false; + } + + if (m_pBitmap != NULL) + { + delete m_pBitmap; + } + + m_pBitmap = pBitmap; + + return true; +} + +bool CHoverPreviewWnd::LoadRtf(long lClipId) +{ + CClipFormat Clip; + + Clip.m_cfType = RegisterClipboardFormat(CF_RTF); + theApp.GetClipData(lClipId, Clip); + + if (Clip.m_hgData == NULL) + { + Clip.Clear(); + return false; + } + + CStringA rtf = Clip.GetAsCStringA(); + + Clip.Free(); + Clip.Clear(); + + if (rtf.IsEmpty()) + { + return false; + } + + // Some apps, ms word for one, put rtf on the clipboard that the drawer + // can not handle. The list cleans that up by pushing the rtf through a + CString csBetterRTF = rtf; + + if (m_pRtfNormalizer == NULL) + { + if (::IsWindow(GetSafeHwnd())) + { + m_pRtfNormalizer = new CRichEditCtrlEx; + + if (m_pRtfNormalizer->Create(_T(""), _T(""), WS_CHILD | WS_VSCROLL | + WS_HSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_NOHIDESEL | + ES_AUTOHSCROLL, CRect(0, 0, 0, 0), this, -1) == FALSE) + { + delete m_pRtfNormalizer; + m_pRtfNormalizer = NULL; + } + } + } + + if (m_pRtfNormalizer != NULL && m_pRtfNormalizer->m_hWnd != NULL) + { + m_pRtfNormalizer->SetRTF(rtf); + + CString csCleaned = m_pRtfNormalizer->GetRTF(); + if (csCleaned.IsEmpty() == FALSE) + { + csBetterRTF = csCleaned; + } + } + + if (m_pRtfDraw == NULL) + { + m_pRtfDraw = new CFormattedTextDraw; + m_pRtfDraw->Create(); + } + + if (m_pRtfDraw == NULL) + { + return false; + } + + CComBSTR bStr(csBetterRTF); + + // put_RTFText fails when the renderer could not be created, in that case + if (m_pRtfDraw->put_RTFText(bStr) != S_OK) + { + return false; + } + + return true; +} + +bool CHoverPreviewWnd::HasClipFormat(long lClipId, CLIPFORMAT cfType) +{ + CClipFormat Clip; + + Clip.m_cfType = cfType; + theApp.GetClipData(lClipId, Clip); + + const bool bHas = (Clip.m_hgData != NULL); + + Clip.Free(); + Clip.Clear(); + + return bHas; +} + +// The list stores html text with unprintable markers in it: \x01\x05\x02 is +// a line break and \x01\x04 .... \x02 carries a colour. DrawText would show +void CHoverPreviewWnd::CleanDisplayText(CString& csText) const +{ + CString csCleaned; + + for (int i = 0; i < csText.GetLength(); i++) + { + const TCHAR c = csText[i]; + + if (c == _T('\x01')) + { + CString csMarker; + + i++; + while (i < csText.GetLength() && csText[i] != _T('\x02')) + { + csMarker += csText[i]; + i++; + } + + if (csMarker.Find(_T('\x05')) >= 0) + { + csCleaned += _T("\r\n"); + } + + continue; + } + + if (c == _T('\x02')) + { + continue; + } + + if (c < _T(' ') && c != _T('\r') && c != _T('\n') && c != _T('\t')) + { + continue; + } + + csCleaned += c; + } + + while (csCleaned.Replace(_T("\r\n\r\n\r\n"), _T("\r\n\r\n")) > 0) + { + } + + csCleaned.TrimRight(); + csText = csCleaned; +} + +bool CHoverPreviewWnd::LoadContent(long lClipId, const CString& csInfo, bool bHasColorCode, COLORREF crColorCode, int nColorAlpha) +{ + m_kind = KIND_NONE; + m_lClipId = lClipId; + m_csInfoExtra.Empty(); + + if (LoadImage(lClipId)) + { + m_kind = KIND_IMAGE; + BuildImageInfo(lClipId); + return true; + } + + if (LoadRtf(lClipId)) + { + m_kind = KIND_RTF; + return true; + } + + if (bHasColorCode) + { + m_kind = KIND_COLOR; + m_crColor = crColorCode; + m_nColorAlpha = nColorAlpha; + return true; + } + + if (HasClipFormat(lClipId, GetFormatID(_T("HTML Format")))) + { + // the preview of html is the clip text with the internal markers + // turned into line breaks, Layout picks that up from the clip text so + CString csHtml = csInfo; + CleanDisplayText(csHtml); + + if (csHtml.IsEmpty() == FALSE) + { + m_kind = KIND_HTML; + return true; + } + } + + if (csInfo.IsEmpty() == FALSE) + { + m_kind = KIND_TEXT; + return true; + } + + return false; +} + +void CHoverPreviewWnd::UpdateFont() +{ + LOGFONT lf; + CGetSetOptions::GetFont(lf); + + lf.lfHeight = m_dpi.Scale(lf.lfHeight); + + m_font.DeleteObject(); + m_font.CreateFontIndirect(&lf); +} + +int CHoverPreviewWnd::MeasureTextHeight(const CString& csText, int nWidth, int nMaxHeight) +{ + if (csText.IsEmpty() || nWidth <= 0) + { + return 0; + } + + CClientDC dc(this); + CFont* pOldFont = dc.SelectObject(&m_font); + + CRect rcMeasure(0, 0, nWidth, 0); + dc.DrawText(csText, &rcMeasure, DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX | DT_EXPANDTABS | DT_CALCRECT); + + dc.SelectObject(pOldFont); + + return min(rcMeasure.Height(), nMaxHeight); +} + +// Width of the longest line of the text, used to make the popup as narrow as +int CHoverPreviewWnd::MeasureTextWidth(const CString& csText) +{ + if (csText.IsEmpty()) + { + return 0; + } + + CClientDC dc(this); + CFont* pOldFont = dc.SelectObject(&m_font); + + int nWidth = 0; + int nStart = 0; + + while (nStart <= csText.GetLength()) + { + int nEnd = csText.Find(_T('\n'), nStart); + if (nEnd < 0) + { + nEnd = csText.GetLength(); + } + + CString csLine = csText.Mid(nStart, nEnd - nStart); + csLine.TrimRight(_T('\r')); + csLine.Replace(_T("\t"), _T(" ")); + + if (csLine.IsEmpty() == FALSE) + { + CSize size = dc.GetTextExtent(csLine); + nWidth = max(nWidth, (int)size.cx); + } + + nStart = nEnd + 1; + } + + dc.SelectObject(pOldFont); + + return nWidth; +} + +void CHoverPreviewWnd::WrapTextToWidth(CString& csText, int nWidth) +{ + if (csText.IsEmpty() || nWidth <= 0) + { + return; + } + + CClientDC dc(this); + CFont* pOldFont = dc.SelectObject(&m_font); + + const int nLength = csText.GetLength(); + CString csWrapped; + int nStart = 0; + + while (nStart < nLength) + { + int nEnd = nStart; + int nSpace = -1; + + while (nEnd < nLength) + { + const TCHAR c = csText[nEnd]; + + if (c == _T('\r') || c == _T('\n')) + { + break; + } + + if (c == _T(' ') || c == _T('\t')) + { + nSpace = nEnd; + } + + if (nEnd > nStart && dc.GetTextExtent(csText.Mid(nStart, nEnd - nStart + 1)).cx > nWidth) + { + break; + } + + nEnd++; + } + + if (nEnd >= nLength || csText[nEnd] == _T('\r') || csText[nEnd] == _T('\n')) + { + csWrapped += csText.Mid(nStart, nEnd - nStart); + + while (nEnd < nLength && (csText[nEnd] == _T('\r') || csText[nEnd] == _T('\n'))) + { + csWrapped += csText[nEnd]; + nEnd++; + } + + nStart = nEnd; + continue; + } + + // break the line, at the last space when there is one, otherwise in + const int nBreak = (nSpace > nStart) ? nSpace : nEnd; + + csWrapped += csText.Mid(nStart, nBreak - nStart); + csWrapped += _T("\r\n"); + + nStart = (nSpace > nStart) ? (nBreak + 1) : nBreak; + } + + dc.SelectObject(pOldFont); + + csText = csWrapped; +} + +void CHoverPreviewWnd::ScaleBitmapTo(int nWidth, int nHeight) +{ + if (m_pBitmap == NULL || nWidth <= 0 || nHeight <= 0) + { + return; + } + + if ((int)m_pBitmap->GetWidth() <= nWidth && (int)m_pBitmap->GetHeight() <= nHeight) + { + return; + } + + Gdiplus::Bitmap* pScaled = new Gdiplus::Bitmap(nWidth, nHeight, PixelFormat32bppPARGB); + if (pScaled == NULL) + { + return; + } + + { + Gdiplus::Graphics graphics(pScaled); + graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic); + graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHalf); + graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver); + + graphics.DrawImage(m_pBitmap, + Gdiplus::Rect(0, 0, nWidth, nHeight), + 0, 0, + (INT)m_pBitmap->GetWidth(), (INT)m_pBitmap->GetHeight(), + Gdiplus::UnitPixel); + } + + delete m_pBitmap; + m_pBitmap = pScaled; +} + +//--------------------------------------------------------------------------- +// exif +static CString GetExifText(Gdiplus::Bitmap* pBitmap, PROPID propId) +{ + CString csValue; + + if (pBitmap == NULL) + { + return csValue; + } + + const UINT nSize = pBitmap->GetPropertyItemSize(propId); + if (nSize == 0) + { + return csValue; + } + + Gdiplus::PropertyItem* pItem = (Gdiplus::PropertyItem*)malloc(nSize); + if (pItem == NULL) + { + return csValue; + } + + if (pBitmap->GetPropertyItem(propId, nSize, pItem) == Gdiplus::Ok && + pItem->type == PropertyTagTypeASCII && + pItem->value != NULL && + pItem->length > 1) + { + csValue = CString((const char*)pItem->value); + csValue.Trim(); + } + + free(pItem); + + return csValue; +} + +static bool GetExifNumber(Gdiplus::Bitmap* pBitmap, PROPID propId, double& dValue) +{ + bool bFound = false; + + if (pBitmap == NULL) + { + return false; + } + + const UINT nSize = pBitmap->GetPropertyItemSize(propId); + if (nSize == 0) + { + return false; + } + + Gdiplus::PropertyItem* pItem = (Gdiplus::PropertyItem*)malloc(nSize); + if (pItem == NULL) + { + return false; + } + + if (pBitmap->GetPropertyItem(propId, nSize, pItem) == Gdiplus::Ok && pItem->value != NULL) + { + if (pItem->type == PropertyTagTypeRational && pItem->length >= 8) + { + const long* pValues = (const long*)pItem->value; + if (pValues[1] != 0) + { + dValue = (double)pValues[0] / (double)pValues[1]; + bFound = true; + } + } + else if (pItem->type == PropertyTagTypeShort && pItem->length >= 2) + { + dValue = (double)*(const unsigned short*)pItem->value; + bFound = true; + } + } + + free(pItem); + + return bFound; +} + +bool CHoverPreviewWnd::LoadExif(long lClipId, CString& csText) +{ + CClipFormat Clip; + + Clip.m_cfType = GetFormatID(_T("JFIF")); + theApp.GetClipData(lClipId, Clip); + + if (Clip.m_hgData == NULL) + { + Clip.Clear(); + return false; + } + + Gdiplus::Bitmap* pBitmap = BitmapImageHelper::GdipImageFromHGLOBAL(Clip.m_hgData); + + Clip.Free(); + Clip.Clear(); + + if (pBitmap == NULL) + { + return false; + } + + CString csLines; + + CString csMake = GetExifText(pBitmap, PropertyTagEquipMake); + CString csModel = GetExifText(pBitmap, PropertyTagEquipModel); + + if (csMake.IsEmpty() == FALSE || csModel.IsEmpty() == FALSE) + { + CString csCamera = csMake; + + if (csCamera.IsEmpty() == FALSE && csModel.IsEmpty() == FALSE) + { + csCamera += _T(" "); + } + + csCamera += csModel; + csLines += csCamera; + } + + CString csTaken = GetExifText(pBitmap, PropertyTagExifDTOrig); + if (csTaken.IsEmpty()) + { + csTaken = GetExifText(pBitmap, PropertyTagDateTime); + } + + if (csTaken.IsEmpty() == FALSE) + { + if (csLines.IsEmpty() == FALSE) + { + csLines += _T("\r\n"); + } + + csLines += csTaken; + } + + CString csValues; + CString csPart; + double dValue = 0; + + if (GetExifNumber(pBitmap, PropertyTagExifExposureTime, dValue) && dValue > 0) + { + if (dValue < 1.0) + { + csPart.Format(_T("1/%.0f s"), 1.0 / dValue); + } + else + { + csPart.Format(_T("%.1f s"), dValue); + } + + csValues += csPart; + } + + if (GetExifNumber(pBitmap, PropertyTagExifFNumber, dValue) && dValue > 0) + { + csPart.Format(_T("f/%.1f"), dValue); + + if (csValues.IsEmpty() == FALSE) + { + csValues += _T(" "); + } + + csValues += csPart; + } + + if (GetExifNumber(pBitmap, PropertyTagExifISOSpeed, dValue) && dValue > 0) + { + csPart.Format(_T("ISO %.0f"), dValue); + + if (csValues.IsEmpty() == FALSE) + { + csValues += _T(" "); + } + + csValues += csPart; + } + + if (GetExifNumber(pBitmap, PropertyTagExifFocalLength, dValue) && dValue > 0) + { + csPart.Format(_T("%.0f mm"), dValue); + + if (csValues.IsEmpty() == FALSE) + { + csValues += _T(" "); + } + + csValues += csPart; + } + + if (csValues.IsEmpty() == FALSE) + { + if (csLines.IsEmpty() == FALSE) + { + csLines += _T("\r\n"); + } + + csLines += csValues; + } + + delete pBitmap; + + if (csLines.IsEmpty()) + { + return false; + } + + csText = csLines; + + return true; +} + +void CHoverPreviewWnd::BuildImageInfo(long lClipId) +{ + m_csInfoExtra.Empty(); + + if (m_pBitmap == NULL) + { + return; + } + + CString csInfo; + csInfo.Format(_T("%u x %u"), m_pBitmap->GetWidth(), m_pBitmap->GetHeight()); + + if (HasClipFormat(lClipId, theApp.m_PNG_Format)) + { + csInfo += _T(" PNG"); + } + else + { + csInfo += _T(" DIB"); + } + + m_csInfoExtra = csInfo; + + CString csExif; + if (LoadExif(lClipId, csExif)) + { + m_csInfoExtra += _T("\r\n"); + m_csInfoExtra += csExif; + } +} + +// Height the preview content needs at the given width: a swatch is sized from +// the width, text is measured, and rtf asks its renderer but never gets less +// than the plain text needs (the renderer used to ask for too little and the +// last lines were cut off). +int CHoverPreviewWnd::MeasureContentHeight(int nWidth, int nMaxHeight, const CString& csText) +{ + if (m_kind == KIND_COLOR) + { + int nHeight = nWidth / 2; + nHeight = max(nHeight, m_dpi.Scale(HOVER_PREVIEW_SWATCH_MIN)); + + return min(nHeight, m_dpi.Scale(HOVER_PREVIEW_SWATCH_MAX)); + } + + if (m_kind == KIND_HTML || m_kind == KIND_TEXT) + { + return max(MeasureTextHeight(csText, nWidth, nMaxHeight), m_dpi.Scale(20)); + } + + if (m_kind == KIND_RTF) + { + int nHeight = MeasureTextHeight(csText, nWidth, nMaxHeight); + nHeight += ((nHeight * 40) / 100) + m_dpi.Scale(8); + + long lNatural = 0; + + if (m_pRtfDraw != NULL) + { + m_pRtfDraw->get_NaturalHeight(nWidth, &lNatural); + } + + if (lNatural > nHeight && lNatural <= nMaxHeight) + { + nHeight = (int)lNatural; + } + + return max(min(nHeight, nMaxHeight), m_dpi.Scale(20)); + } + + return m_dpi.Scale(20); +} + +void CHoverPreviewWnd::Layout(const CString& csInfo, const CRect& rcAnchor) +{ + UpdateFont(); + + // The tool tip text the parent hands over is the clip text, an empty line + // and the clip details. Split the two: a text preview shows the clip text + // itself, so only the details belong below the preview. + m_csClipText = csInfo; + CleanDisplayText(m_csClipText); + m_csClipText.TrimRight(); + + m_csDetails.Empty(); + + int nDetailsStart = m_csClipText.Find(_T("Database ID:")); + if (nDetailsStart > 0) + { + m_csDetails = m_csClipText.Mid(nDetailsStart); + m_csClipText = m_csClipText.Left(nDetailsStart); + m_csClipText.TrimRight(); + } + + if (m_csInfoExtra.IsEmpty() == FALSE) + { + if (m_csDetails.IsEmpty() == FALSE) + { + m_csDetails += _T("\r\n"); + } + + m_csDetails += m_csInfoExtra; + } + + if (m_kind == KIND_TEXT || m_kind == KIND_HTML) + { + m_csHtmlText = m_csClipText; + } + + // what is drawn below the preview + if (m_kind == KIND_IMAGE) + { + m_csInfo = csInfo; + CleanDisplayText(m_csInfo); + m_csInfo.TrimRight(); + + if (m_csInfoExtra.IsEmpty() == FALSE) + { + if (m_csInfo.IsEmpty() == FALSE) + { + m_csInfo += _T("\r\n"); + } + + m_csInfo += m_csInfoExtra; + } + } + else if (m_kind == KIND_COLOR) + { + CString csCode = m_csClipText; + int nLineEnd = csCode.Find(_T("\r\n")); + if (nLineEnd > 0) + { + csCode = csCode.Left(nLineEnd); + } + + m_csInfo = csCode; + + if (m_csDetails.IsEmpty() == FALSE) + { + if (m_csInfo.IsEmpty() == FALSE) + { + m_csInfo += _T("\r\n"); + } + + m_csInfo += m_csDetails; + } + } + else + { + m_csInfo = m_csDetails; + } + + const int nMargin = m_dpi.Scale(HOVER_PREVIEW_MARGIN); + const int nGap = m_dpi.Scale(HOVER_PREVIEW_TEXT_GAP); + const int nPopupGap = m_dpi.Scale(HOVER_PREVIEW_POPUP_GAP); + const int nMinWidth = m_dpi.Scale(HOVER_PREVIEW_MIN_WIDTH); + + CRect rcWork = MonitorRectFromRect(CRect(rcAnchor.left, rcAnchor.top, rcAnchor.left + 1, rcAnchor.top + 1)); + + const int nWidth = CGetSetOptions::GetToolTipPreviewWidth(); + const int nMaxContentWidth = rcWork.Width() - (2 * nMargin); + const int nMaxContentHeight = (rcWork.Height() * 3) / 4; + + int nContentWidth = 0; + int nContentHeight = 0; + int nImageWidth = 0; + int nImageHeight = 0; + + if (m_kind == KIND_IMAGE && m_pBitmap != NULL) + { + int nMaxImageWidth = 0; + int nMaxImageHeight = 0; + + if (nWidth <= 0) + { + // "Original size": only shrink to what fits on the monitor + nMaxImageWidth = nMaxContentWidth; + nMaxImageHeight = rcWork.Height() - (2 * nMargin); + } + else + { + // the width from the menu is the box the image has to fit in + nMaxImageWidth = m_dpi.Scale(nWidth); + nMaxImageHeight = min(m_dpi.Scale(nWidth), nMaxContentHeight); + } + + const double dBitmapWidth = (double)m_pBitmap->GetWidth(); + const double dBitmapHeight = (double)m_pBitmap->GetHeight(); + + if (dBitmapWidth > 0 && dBitmapHeight > 0) + { + // never scale up: an image that fits the box is shown at its size + double dScale = min(min(nMaxImageWidth / dBitmapWidth, nMaxImageHeight / dBitmapHeight), 1.0); + + nImageWidth = (int)((dBitmapWidth * dScale) + 0.5); + nImageHeight = (int)((dBitmapHeight * dScale) + 0.5); + + // decode to the size that gets drawn, holding the full size decode + // while only the scaled copy is drawn wastes a lot of memory + ScaleBitmapTo(nImageWidth, nImageHeight); + } + + // the box is only as wide as the widest thing drawn: the image or a + // line of the information below it + int nInfoWidth = MeasureTextWidth(m_csInfo); + + if (nInfoWidth > 0) + { + nInfoWidth = min(nInfoWidth + m_dpi.Scale(HOVER_PREVIEW_WIDTH_SLACK), nMaxImageWidth); + } + + nContentWidth = max(nImageWidth, nInfoWidth); + nContentHeight = nImageHeight; + } + else + { + // the width is the widest line that is drawn, capped by the width from + // the menu; for rtf and colour only the clip text counts, the details + // are drawn below and may wrap + int nTierWidth = (nWidth > 0) ? m_dpi.Scale(nWidth) : m_dpi.Scale(HOVER_PREVIEW_DEFAULT_WIDTH); + nTierWidth = min(nTierWidth, nMaxContentWidth); + + CString csContentText = (m_kind == KIND_HTML || m_kind == KIND_TEXT) ? m_csHtmlText : m_csClipText; + + if (m_kind == KIND_COLOR) + { + int nLineEnd = csContentText.Find(_T("\r\n")); + if (nLineEnd > 0) + { + csContentText = csContentText.Left(nLineEnd); + } + } + + int nNaturalWidth = max(MeasureTextWidth(csContentText), MeasureTextWidth(m_csInfo)); + + if (m_kind == KIND_RTF) + { + // rich text uses its own fonts, which are usually wider than the + // plain text measurement, and the renderer knows what it wants + nNaturalWidth += ((nNaturalWidth * 30) / 100); + + long lWantedWidth = 0; + + if (m_pRtfDraw != NULL) + { + m_pRtfDraw->get_NaturalWidth(10000, &lWantedWidth); + } + + if (lWantedWidth > nNaturalWidth && lWantedWidth < nTierWidth) + { + nNaturalWidth = (int)lWantedWidth; + } + } + + if (nNaturalWidth <= 0) + { + nContentWidth = nTierWidth; + } + else + { + nContentWidth = min(max(nNaturalWidth + m_dpi.Scale(HOVER_PREVIEW_WIDTH_SLACK), nMinWidth), nTierWidth); + } + + // wrap before measuring: one long word turns into several lines + if (m_kind == KIND_TEXT || m_kind == KIND_HTML) + { + WrapTextToWidth(m_csHtmlText, nContentWidth); + csContentText = m_csHtmlText; + } + + nContentHeight = MeasureContentHeight(nContentWidth, nMaxContentHeight, csContentText); + } + + m_rcContent.SetRect(nMargin, nMargin, nMargin + nContentWidth, nMargin + nContentHeight); + + WrapTextToWidth(m_csInfo, nContentWidth); + + int nTextHeight = 0; + int nDetailsGap = nGap; + + if (m_csInfo.IsEmpty() == FALSE) + { + TEXTMETRIC tm; + { + CClientDC dc(this); + CFont* pOldFont = dc.SelectObject(&m_font); + dc.GetTextMetrics(&tm); + dc.SelectObject(pOldFont); + } + + // a text preview and the information are both text, keep them apart + if (m_kind != KIND_IMAGE && m_kind != KIND_COLOR) + { + nDetailsGap = nGap + (2 * tm.tmHeight); + } + + int nMaxTextHeight = max(rcWork.Height() - (2 * nMargin) - nContentHeight - nDetailsGap, tm.tmHeight * 3); + const int nMaxLineHeight = tm.tmHeight * max(HOVER_PREVIEW_MIN_TEXT_LINES, CGetSetOptions::GetMaxToolTipLines()); + + nTextHeight = MeasureTextHeight(m_csInfo, nContentWidth, min(nMaxTextHeight, nMaxLineHeight)); + } + + if (nTextHeight > 0) + { + m_rcText.SetRect(nMargin, + nMargin + nContentHeight + nDetailsGap, + nMargin + nContentWidth, + nMargin + nContentHeight + nDetailsGap + nTextHeight); + } + else + { + m_rcText.SetRectEmpty(); + } + + const int nPopupWidth = nContentWidth + (2 * nMargin); + const int nPopupHeight = nMargin + nContentHeight + (nTextHeight > 0 ? (nDetailsGap + nTextHeight) : 0) + nMargin; + + // next to the mouse pointer, flipped to the other side when it would not + // fit on the monitor + CPoint ptCursor; + GetCursorPos(&ptCursor); + + int x = ptCursor.x + nPopupGap; + if (x + nPopupWidth > rcWork.right - nMargin) + { + x = ptCursor.x - nPopupWidth - nPopupGap; + } + if (x < rcWork.left + nMargin) + { + x = rcWork.left + nMargin; + } + + int y = ptCursor.y + nPopupGap; + if (y + nPopupHeight > rcWork.bottom - nMargin) + { + y = ptCursor.y - nPopupHeight - nPopupGap; + } + if (y < rcWork.top + nMargin) + { + y = rcWork.top + nMargin; + } + + // never under the mouse pointer: a mouse leave would close the popup and + // put the pointer back on the list, over and over + CRect rcPopup(x, y, x + nPopupWidth, y + nPopupHeight); + + if (rcPopup.PtInRect(ptCursor)) + { + if (ptCursor.x - nPopupWidth - nPopupGap >= rcWork.left + nMargin) + { + x = ptCursor.x - nPopupWidth - nPopupGap; + } + else if (ptCursor.y - nPopupHeight - nPopupGap >= rcWork.top + nMargin) + { + y = ptCursor.y - nPopupHeight - nPopupGap; + } + else if (ptCursor.y + nPopupGap + nPopupHeight <= rcWork.bottom - nMargin) + { + x = ptCursor.x + nPopupGap; + y = ptCursor.y + nPopupGap; + } + } + + SetWindowPos(&wndTopMost, x, y, nPopupWidth, nPopupHeight, SWP_NOACTIVATE | SWP_SHOWWINDOW); + Invalidate(FALSE); +} + +bool CHoverPreviewWnd::ShowForClip(CWnd* pOwner, long lClipId, const CString& csInfo, const CRect& rcAnchor, + bool bHasColorCode, COLORREF crColorCode, int nColorAlpha) +{ + CHoverPreviewWnd* pWnd = Get(); + + if (pWnd->CreateIfNeeded(pOwner) == false) + { + return false; + } + + if (pWnd->LoadContent(lClipId, csInfo, bHasColorCode, crColorCode, nColorAlpha) == false) + { + HideIfVisible(); + return false; + } + + pWnd->Layout(csInfo, rcAnchor); + + return true; +} + +void CHoverPreviewWnd::ReleaseContent() +{ + if (m_pBitmap != NULL) + { + delete m_pBitmap; + m_pBitmap = NULL; + } + + m_kind = KIND_NONE; + m_lClipId = -1; + m_csHtmlText.Empty(); + m_csClipText.Empty(); + m_csDetails.Empty(); + m_csInfo.Empty(); + m_csInfoExtra.Empty(); + m_rcContent.SetRectEmpty(); + m_rcText.SetRectEmpty(); +} + +void CHoverPreviewWnd::HideIfVisible() +{ + if (s_pInstance == NULL) + { + return; + } + + if (::IsWindow(s_pInstance->GetSafeHwnd()) == FALSE) + { + return; + } + + if (::IsWindowVisible(s_pInstance->GetSafeHwnd())) + { + s_pInstance->ShowWindow(SW_HIDE); + s_pInstance->ReleaseContent(); + } +} + +bool CHoverPreviewWnd::IsShowingClip(long lClipId) +{ + if (s_pInstance == NULL || lClipId <= 0) + { + return false; + } + + if (::IsWindow(s_pInstance->GetSafeHwnd()) == FALSE || + ::IsWindowVisible(s_pInstance->GetSafeHwnd()) == FALSE) + { + return false; + } + + return s_pInstance->m_lClipId == lClipId; +} + +BOOL CHoverPreviewWnd::OnEraseBkgnd(CDC* pDC) +{ + // OnPaint fills the whole client area + return TRUE; +} + +void CHoverPreviewWnd::DrawCheckerboard(CDC* pDC, const CRect& rc) +{ + const int nSquare = 8; + int nIndex = 0; + + for (int y = rc.top; y < rc.bottom; y += nSquare) + { + for (int x = rc.left; x < rc.right; x += nSquare, nIndex++) + { + CRect rcSquare(x, y, min(x + nSquare, rc.right), min(y + nSquare, rc.bottom)); + + pDC->FillSolidRect(rcSquare, ((nIndex % 2) == 0) ? RGB(255, 255, 255) : RGB(204, 204, 204)); + } + + nIndex++; + } +} + +void CHoverPreviewWnd::OnPaint() +{ + CPaintDC dc(this); + + CRect rcClient; + GetClientRect(&rcClient); + + dc.FillSolidRect(rcClient, CGetSetOptions::m_Theme.DescriptionWindowBG()); + + CFont* pOldFont = dc.SelectObject(&m_font); + dc.SetBkMode(TRANSPARENT); + + switch (m_kind) + { + case KIND_IMAGE: + { + if (m_pBitmap != NULL && m_rcContent.IsRectEmpty() == FALSE) + { + Gdiplus::Graphics graphics(dc.GetSafeHdc()); + graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic); + graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHalf); + graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver); + + // Draw it at its own size, centred in the content box. The box is + // only wider than the image when the image is narrower than the + const int nDrawWidth = min((int)m_pBitmap->GetWidth(), m_rcContent.Width()); + const int nDrawHeight = min((int)m_pBitmap->GetHeight(), m_rcContent.Height()); + const int nDrawLeft = m_rcContent.left + ((m_rcContent.Width() - nDrawWidth) / 2); + + Gdiplus::ImageAttributes attrs; + Gdiplus::Rect dest(nDrawLeft, m_rcContent.top, nDrawWidth, nDrawHeight); + + graphics.DrawImage(m_pBitmap, + dest, + 0, 0, + (INT)m_pBitmap->GetWidth(), (INT)m_pBitmap->GetHeight(), + Gdiplus::UnitPixel, + &attrs); + } + } + break; + + case KIND_RTF: + { + if (m_pRtfDraw != NULL && m_rcContent.IsRectEmpty() == FALSE) + { + RECT rc = m_rcContent; + m_pRtfDraw->Draw(dc.GetSafeHdc(), &rc); + } + } + break; + + case KIND_COLOR: + { + if (m_rcContent.IsRectEmpty() == FALSE) + { + if (m_nColorAlpha < 255) + { + // let the checkerboard show through, the same way the list + DrawCheckerboard(&dc, m_rcContent); + + Gdiplus::Graphics graphics(dc.GetSafeHdc()); + Gdiplus::Color gdiplusColor(m_nColorAlpha, GetRValue(m_crColor), GetGValue(m_crColor), GetBValue(m_crColor)); + Gdiplus::SolidBrush brush(gdiplusColor); + graphics.FillRectangle(&brush, Gdiplus::Rect(m_rcContent.left, m_rcContent.top, m_rcContent.Width(), m_rcContent.Height())); + } + else + { + dc.FillSolidRect(m_rcContent, m_crColor); + } + + dc.Draw3dRect(m_rcContent, CGetSetOptions::m_Theme.Border(), CGetSetOptions::m_Theme.Border()); + } + } + break; + + case KIND_HTML: + case KIND_TEXT: + { + if (m_csHtmlText.IsEmpty() == FALSE && m_rcContent.IsRectEmpty() == FALSE) + { + dc.SetTextColor(CGetSetOptions::m_Theme.DescriptionWindowText()); + dc.DrawText(m_csHtmlText, &m_rcContent, DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX | DT_EXPANDTABS); + } + } + break; + + default: + break; + } + + if (m_csInfo.IsEmpty() == FALSE && m_rcText.IsRectEmpty() == FALSE) + { + // the rtf renderer draws straight into this dc and can leave its own + // font and colours selected, so put ours back first + dc.SelectObject(&m_font); + dc.SetBkMode(TRANSPARENT); + dc.SetTextColor(CGetSetOptions::m_Theme.DescriptionWindowText()); + dc.DrawText(m_csInfo, &m_rcText, DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX | DT_EDITCONTROL); + } + + dc.SelectObject(pOldFont); + + dc.Draw3dRect(rcClient, CGetSetOptions::m_Theme.Border(), CGetSetOptions::m_Theme.Border()); +} diff --git a/src/HoverPreviewWnd.h b/src/HoverPreviewWnd.h new file mode 100644 index 00000000..a66a5f36 --- /dev/null +++ b/src/HoverPreviewWnd.h @@ -0,0 +1,99 @@ +#pragma once + +#include "DPI.h" + +class CRichEditCtrlEx; +struct IFormattedTextDraw; + +//--------------------------------------------------------------------------- +// CHoverPreviewWnd +// +// Own hover preview popup for the quick paste list. +// +// This is deliberately NOT the existing description window (CToolTipEx). +class CHoverPreviewWnd : public CWnd +{ +public: + // Shows the preview of lClipId with csInfo drawn underneath for images + // and colours. rcAnchor is the screen rectangle of the list row the + // mouse is over. bHasColorCode tells the popup that the clip text is a + // colour code, which only the list control can work out. Returns false + // when the clip has nothing to preview. + static bool ShowForClip(CWnd* pOwner, long lClipId, const CString& csInfo, const CRect& rcAnchor, + bool bHasColorCode, COLORREF crColorCode, int nColorAlpha); + + // Hides the popup when it is visible, otherwise does nothing. + static void HideIfVisible(); + + // true when the popup is visible and currently showing lClipId. + static bool IsShowingClip(long lClipId); + +private: + enum HoverPreviewKind + { + KIND_NONE = 0, + KIND_IMAGE, + KIND_RTF, + KIND_COLOR, + KIND_HTML, + // nothing but text, the popup then shows the text the tool tip bubble + // would have shown + KIND_TEXT + }; + + CHoverPreviewWnd(); + virtual ~CHoverPreviewWnd(); + + // Returns the one instance, created on first use and kept for the life + // time of the process. + static CHoverPreviewWnd* Get(); + + bool CreateIfNeeded(CWnd* pOwner); + bool LoadContent(long lClipId, const CString& csInfo, bool bHasColorCode, COLORREF crColorCode, int nColorAlpha); + bool LoadImage(long lClipId); + bool LoadRtf(long lClipId); + bool HasClipFormat(long lClipId, CLIPFORMAT cfType); + // pixel size of the image plus, when the clip happens to hold jpeg data + // as well, the exif of that jpeg + void BuildImageInfo(long lClipId); + bool LoadExif(long lClipId, CString& csText); + void CleanDisplayText(CString& csText) const; + // Frees the decoded content, called when the popup is hidden so that a + // decoded image is not held on to while it is not shown + void ReleaseContent(); + void UpdateFont(); + void Layout(const CString& csInfo, const CRect& rcAnchor); + int MeasureTextHeight(const CString& csText, int nWidth, int nMaxHeight); + int MeasureTextWidth(const CString& csText); + int MeasureContentHeight(int nWidth, int nMaxHeight, const CString& csText); + // DrawText only breaks lines at spaces, so a single long word (a url, a + // path, a line of code) has to be wrapped first or it runs past the edge + void WrapTextToWidth(CString& csText, int nWidth); + // Replaces the decoded image with a copy at the given size. Only the + // scaled copy is ever drawn, so holding on to the full size decode would + void ScaleBitmapTo(int nWidth, int nHeight); + void DrawCheckerboard(CDC* pDC, const CRect& rc); + + afx_msg void OnPaint(); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + DECLARE_MESSAGE_MAP() + + static CHoverPreviewWnd* s_pInstance; + + HoverPreviewKind m_kind; + Gdiplus::Bitmap* m_pBitmap; + IFormattedTextDraw* m_pRtfDraw; + CRichEditCtrlEx* m_pRtfNormalizer; + COLORREF m_crColor; + int m_nColorAlpha; + CString m_csHtmlText; + CString m_csClipText; + CString m_csDetails; + CString m_csInfo; + CString m_csInfoExtra; + CRect m_rcContent; + CRect m_rcText; + CFont m_font; + CDPI m_dpi; + long m_lClipId; +}; diff --git a/src/Options.cpp b/src/Options.cpp index 3bb1a932..e95f9737 100644 --- a/src/Options.cpp +++ b/src/Options.cpp @@ -2924,6 +2924,26 @@ void CGetSetOptions::SetMaxToolTipCharacters(int val) SetProfileLong("MaxToolTipCharacters", val); } +int CGetSetOptions::GetToolTipPreviewWidth() +{ + return GetProfileLong("ToolTipPreviewWidth", 400); +} + +void CGetSetOptions::SetToolTipPreviewWidth(int val) +{ + SetProfileLong("ToolTipPreviewWidth", val); +} + +BOOL CGetSetOptions::GetToolTipPreviewEnabled() +{ + return GetProfileLong("ToolTipPreviewEnabled", 0); +} + +void CGetSetOptions::SetToolTipPreviewEnabled(BOOL bEnabled) +{ + SetProfileLong("ToolTipPreviewEnabled", bEnabled); +} + int CGetSetOptions::GetDoubleKeyStrokeTimeout() { return GetProfileLong("DoubleKeyStrokeTimeout", 350); diff --git a/src/Options.h b/src/Options.h index 9f1196f7..ece5f7b1 100644 --- a/src/Options.h +++ b/src/Options.h @@ -635,6 +635,16 @@ class CGetSetOptions static int GetMaxToolTipCharacters(); static void SetMaxToolTipCharacters(int val); + // Width of the hover preview popup of the quick paste list, 0 means the + // image is shown in its original size. + static int GetToolTipPreviewWidth(); + static void SetToolTipPreviewWidth(int val); + + // When on, the quick paste list shows the hover preview popup instead of + // the plain tool tip bubble. + static BOOL GetToolTipPreviewEnabled(); + static void SetToolTipPreviewEnabled(BOOL bEnabled); + static int m_doubleKeyStrokeTimeout; static int GetDoubleKeyStrokeTimeout(); static void SetDoubleKeyStrokeTimeout(int val); diff --git a/src/QListCtrl.cpp b/src/QListCtrl.cpp index 3c9cf307..f4f4f5de 100644 --- a/src/QListCtrl.cpp +++ b/src/QListCtrl.cpp @@ -4,6 +4,7 @@ #include "stdafx.h" #include "CP_Main.h" #include "QListCtrl.h" +#include "HoverPreviewWnd.h" #include "ProcessPaste.h" #include "BitmapHelper.h" #include "MainTableFunctions.h" @@ -32,6 +33,10 @@ static char THIS_FILE[] = __FILE__; #define TIMER_SHOW_PROPERTIES 1 #define TIMER_HIDE_SCROL 2 #define TIMER_SHOW_SCROLL 3 +#define TIMER_HOVER_PREVIEW 4 + +// delay before the hover preview popup is shown for the row under the pointer +#define HOVER_PREVIEW_DELAY 300 #define VALID_TOOLTIP (m_pToolTip && ::IsWindow(m_pToolTip->m_hWnd)) @@ -213,6 +218,10 @@ CQListCtrl::CQListCtrl() m_showIfClipWasPasted = TRUE; m_bShowTextForFirstTenHotKeys = true; m_pToolTipActions = NULL; + m_nHoverRow = -1; + m_bTrackingMouse = false; + m_hwndStdToolTip = NULL; + m_bStdToolTipSuppressed = false; } CQListCtrl::~CQListCtrl() @@ -273,6 +282,7 @@ BEGIN_MESSAGE_MAP(CQListCtrl, CListCtrl) ON_WM_KILLFOCUS() ON_WM_MEASUREITEM_REFLECT() ON_WM_MOUSEHWHEEL() + ON_WM_MOUSELEAVE() END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// @@ -280,6 +290,8 @@ END_MESSAGE_MAP() void CQListCtrl::OnKeydown(NMHDR* pNMHDR, LRESULT* pResult) { + StopHoverPreview(); + LV_KEYDOWN* pLVKeyDown = (LV_KEYDOWN*)pNMHDR; *pResult = 0; @@ -818,10 +830,9 @@ void CQListCtrl::DrawCheckerboard(CDC* pDC, CRect rect) } -void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) +bool CQListCtrl::ParseColorCode(const CString& csText, COLORREF& crColor, int& nAlpha, CString& csCleanedText) { - if (CGetSetOptions::m_bDrawCopiedColorCode == FALSE || csText.IsEmpty()) - return; + bool bFound = false; // 1. Initial Cleaning and Prep CString cleanedText = csText; @@ -831,38 +842,18 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) cleanedText.Trim(); if (cleanedText.IsEmpty()) - return; - + return bFound; CString originalCleanedText = cleanedText; CString parseText = cleanedText; parseText.MakeLower(); - // 2. Helper lambda to draw the color box + // 2. Helper lambda, records the parsed colour and the cleaned up text auto DrawColorBox = [&](COLORREF color, int alpha = 255) { - CRect pastedRect(rcText); - int boxSize = rcText.Height(); - pastedRect.right = pastedRect.left + boxSize; - pastedRect.bottom = pastedRect.top + boxSize; - - if (alpha < 255) - { - DrawCheckerboard(pDC, pastedRect); - - // Use GDI+ for alpha blending - Gdiplus::Graphics graphics(pDC->GetSafeHdc()); - Gdiplus::Color gdiplusColor(alpha, GetRValue(color), GetGValue(color), GetBValue(color)); - Gdiplus::SolidBrush brush(gdiplusColor); - graphics.FillRectangle(&brush, Gdiplus::Rect(pastedRect.left, pastedRect.top, pastedRect.Width(), pastedRect.Height())); - } - else - { - // Opaque color is faster with FillSolidRect and doesn't need a checkerboard. - pDC->FillSolidRect(pastedRect, color); - } - - rcText.left += boxSize + m_windowDpi->Scale(ROW_LEFT_BORDER); - csText = originalCleanedText; + crColor = color; + nAlpha = alpha; + csCleanedText = originalCleanedText; + bFound = true; }; // 3. Check for formats with unique signatures first @@ -873,7 +864,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (it != g_colorNameMap.end()) { DrawColorBox(it->second); - return; + return bFound; } // Check for Hex Colors with # or 0x prefix @@ -899,7 +890,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (swscanf(hexString, _T("%2x%2x%2x%2x"), &r, &g, &b, &a) == 4) { DrawColorBox(RGB(r, g, b), a); - return; + return bFound; } } else // length is 6 @@ -907,7 +898,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (swscanf(hexString, _T("%2x%2x%2x"), &r, &g, &b) == 3) { DrawColorBox(RGB(r, g, b)); // default alpha - return; + return bFound; } } } @@ -959,7 +950,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (r_val >= 0 && r_val <= 255 && g_val >= 0 && g_val <= 255 && b_val >= 0 && b_val <= 255) { DrawColorBox(RGB((int)r_val, (int)g_val, (int)b_val), alpha); - return; + return bFound; } } } @@ -1003,7 +994,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) h_val = fmod(h_val, 360.0); if (h_val < 0) h_val += 360.0; DrawColorBox(HslToRgb(h_val, s_val / 100.0, l_val / 100.0), alpha); - return; + return bFound; } } } @@ -1051,7 +1042,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (l_normalized >= 0 && l_normalized <= 1.0 && c_val >= 0) { DrawColorBox(OklchToRgb(l_normalized, c_val, h_val), alpha); - return; + return bFound; } } } @@ -1078,7 +1069,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { DrawColorBox(RGB(r, g, b)); - return; + return bFound; } } } @@ -1090,7 +1081,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { DrawColorBox(RGB(r, g, b)); - return; + return bFound; } } @@ -1100,7 +1091,7 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { DrawColorBox(RGB(r, g, b)); - return; + return bFound; } } @@ -1111,12 +1102,51 @@ void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) if (swscanf(parseText, _T("%2x%2x%2x%n"), &r, &g, &b, &chars_consumed) == 3 && chars_consumed == 6) { DrawColorBox(RGB(r, g, b)); - return; + return bFound; } } + + return bFound; } +void CQListCtrl::DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC) +{ + if (CGetSetOptions::m_bDrawCopiedColorCode == FALSE || csText.IsEmpty()) + return; + + COLORREF color = 0; + int alpha = 255; + CString cleanedText; + if (ParseColorCode(csText, color, alpha, cleanedText) == false) + return; + + // draw the color box next to the text, this is what the old function did + // after it had worked out the color + CRect pastedRect(rcText); + int boxSize = rcText.Height(); + pastedRect.right = pastedRect.left + boxSize; + pastedRect.bottom = pastedRect.top + boxSize; + + if (alpha < 255) + { + DrawCheckerboard(pDC, pastedRect); + + // Use GDI+ for alpha blending + Gdiplus::Graphics graphics(pDC->GetSafeHdc()); + Gdiplus::Color gdiplusColor(alpha, GetRValue(color), GetGValue(color), GetBValue(color)); + Gdiplus::SolidBrush brush(gdiplusColor); + graphics.FillRectangle(&brush, Gdiplus::Rect(pastedRect.left, pastedRect.top, pastedRect.Width(), pastedRect.Height())); + } + else + { + // Opaque color is faster with FillSolidRect and doesn't need a checkerboard. + pDC->FillSolidRect(pastedRect, color); + } + + rcText.left += boxSize + m_windowDpi->Scale(ROW_LEFT_BORDER); + csText = cleanedText; +} BOOL CQListCtrl::DrawRtfText(int nItem, CRect& crRect, CDC* pDC) { if (CGetSetOptions::m_bDrawRTF == FALSE) @@ -1250,6 +1280,10 @@ BOOL CQListCtrl::OnToolTipText(UINT id, NMHDR* pNMHDR, LRESULT* pResult) if (nID == 0) // Notification in NT from automatically return FALSE; // created tooltip + // keep the tooltip control around, the hover preview popup turns it off + // while it shows the same clip + m_hwndStdToolTip = pNMHDR->hwndFrom; + ::SendMessage(pNMHDR->hwndFrom, TTM_SETMAXTIPWIDTH, 0, 500); if (CGetSetOptions::m_tooltipTimeout > 0) @@ -1259,7 +1293,24 @@ BOOL CQListCtrl::OnToolTipText(UINT id, NMHDR* pNMHDR, LRESULT* pResult) // Use Item's name as the tool tip. Change this for something different. // Like use its file size, etc. - GetToolTipText((int)nID - 1, strTipText); + const int nRow = (int)nID - 1; + const bool bValidRow = (nRow >= 0 && nRow < GetItemCount()); + + if (bValidRow && CGetSetOptions::GetToolTipPreviewEnabled()) + { + // the popup shows this clip, and turning the bubble off here is what + // keeps it from flashing up before the popup comes along + SuppressStandardToolTip(true); + } + else + { + SuppressStandardToolTip(false); + + if (bValidRow) + { + GetToolTipText(nRow, strTipText); + } + } //Replace the tabs with spaces, the tooltip didn't like the \t s strTipText.Replace(_T("\t"), _T(" ")); @@ -1521,6 +1572,9 @@ bool CQListCtrl::PostEventLoadedCheckDescription(int updatedRow) bool CQListCtrl::ShowFullDescription(bool bFromAuto, bool fromNextPrev) { + // the description window and the hover preview popup would cover each other + StopHoverPreview(); + if (this->GetSelectedCount() == 0) { return false; @@ -1931,6 +1985,8 @@ void CQListCtrl::OnSelectionChange(NMHDR* pNMHDR, LRESULT* pResult) { NMLISTVIEW* pnmv = (NMLISTVIEW*)pNMHDR; + StopHoverPreview(); + if ((pnmv->uNewState == 3) || (pnmv->uNewState == 1)) { @@ -1990,6 +2046,16 @@ void CQListCtrl::OnTimer(UINT_PTR nIDEvent) } break; + case TIMER_HOVER_PREVIEW: + { + KillTimer(TIMER_HOVER_PREVIEW); + + ShowHoverPreview(m_nHoverRow); + + callBase = false; + } + break; + case TIMER_HIDE_SCROL: { CPoint cursorPos; @@ -2060,6 +2126,8 @@ void CQListCtrl::SetLogFont(LOGFONT& font) void CQListCtrl::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { + StopHoverPreview(); + CListCtrl::OnVScroll(nSBCode, nPos, pScrollBar); // Notify parent to update modern scrollbar @@ -2142,6 +2210,225 @@ void CQListCtrl::OnMouseMove(UINT nFlags, CPoint point) } CListCtrl::OnMouseMove(nFlags, point); + + if ((nFlags & (MK_LBUTTON | MK_RBUTTON | MK_MBUTTON)) == 0) + { + TrackHover(point); + } + else + { + // don't pop up a preview while an item is being dragged + StopHoverPreview(); + } +} + +void CQListCtrl::OnMouseLeave() +{ + m_bTrackingMouse = false; + + StopHoverPreview(); + + Default(); +} + +// Returns the row under the given client point or -1, this is the same search +// the tooltip hit test does. +int CQListCtrl::GetRowFromPoint(CPoint point) +{ + CRect rect; + GetClientRect(&rect); + + if (rect.PtInRect(point) == FALSE) + { + return -1; + } + + const int nCount = GetItemCount(); + if (nCount == 0) + { + return -1; + } + + int nTopIndex = GetTopIndex(); + int nBottomIndex = nTopIndex + GetCountPerPage(); + if (nBottomIndex > nCount) + { + nBottomIndex = nCount; + } + + for (int nIndex = nTopIndex; nIndex <= nBottomIndex; nIndex++) + { + GetItemRect(nIndex, rect, LVIR_BOUNDS); + if (rect.PtInRect(point)) + { + return nIndex; + } + } + + return -1; +} + +// Follows the row under the mouse pointer and starts the delay after which +// the hover preview popup is shown. +void CQListCtrl::TrackHover(CPoint point) +{ + if (CGetSetOptions::GetToolTipPreviewEnabled() == FALSE) + { + // the plain tool tip bubble is in charge + StopHoverPreview(); + return; + } + + if (m_bTrackingMouse == false) + { + // ask for a WM_MOUSELEAVE so that the popup is closed the moment the + // pointer leaves the list + TRACKMOUSEEVENT tme; + memset(&tme, 0, sizeof(tme)); + tme.cbSize = sizeof(TRACKMOUSEEVENT); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = m_hWnd; + tme.dwHoverTime = HOVER_DEFAULT; + + m_bTrackingMouse = (_TrackMouseEvent(&tme) != FALSE); + } + + const int nRow = GetRowFromPoint(point); + + if (nRow != m_nHoverRow) + { + // another row (or no row at all) is under the pointer now, so drop + // what is showing and start the delay again + StopHoverPreview(); + + m_nHoverRow = nRow; + + if (nRow >= 0) + { + SetTimer(TIMER_HOVER_PREVIEW, HOVER_PREVIEW_DELAY, NULL); + } + } +} + +// Shows the hover preview popup of the given row. Only clips with an image +// get the popup, every other row keeps the normal tooltip bubble. +void CQListCtrl::ShowHoverPreview(int nRow) +{ + if (theApp.m_bShowingQuickPaste == false) + { + return; + } + + if (nRow < 0 || nRow >= GetItemCount()) + { + return; + } + + // the pointer may have moved on while the timer was pending + CPoint point; + GetCursorPos(&point); + ScreenToClient(&point); + + if (GetRowFromPoint(point) != nRow) + { + return; + } + + const long lClipId = (long)GetItemData(nRow); + if (lClipId <= 0) + { + return; + } + + CString csInfo; + GetToolTipText(nRow, csInfo); + csInfo.Replace(_T("\t"), _T(" ")); + + // the popup shows a swatch for the colour codes the list draws a swatch + // for in the row, the parsing of those codes is shared with that code + COLORREF crColor = 0; + int nAlpha = 255; + bool bHasColorCode = false; + + CClipFormat Clip; + Clip.m_cfType = CF_UNICODETEXT; + if (GetClipData(nRow, Clip) && Clip.m_hgData) + { + CString csClipText = Clip.GetAsCString(); + Clip.Free(); + Clip.Clear(); + + CString csCleaned; + bHasColorCode = ParseColorCode(csClipText, crColor, nAlpha, csCleaned); + } + else + { + Clip.m_cfType = CF_TEXT; + if (GetClipData(nRow, Clip) && Clip.m_hgData) + { + CString csClipText(Clip.GetAsCStringA()); + Clip.Free(); + Clip.Clear(); + + CString csCleaned; + bHasColorCode = ParseColorCode(csClipText, crColor, nAlpha, csCleaned); + } + } + + CRect rc; + GetItemRect(nRow, rc, LVIR_BOUNDS); + ClientToScreen(rc); + + if (CHoverPreviewWnd::ShowForClip(this, lClipId, csInfo, rc, bHasColorCode, crColor, nAlpha)) + { + // the popup shows what the tooltip bubble would show, so turn the + // bubble off while the popup is up + SuppressStandardToolTip(true); + } + else + { + // nothing to preview at all, let the tool tip bubble have this row + StopHoverPreview(); + } +} + +void CQListCtrl::StopHoverPreview() +{ + KillTimer(TIMER_HOVER_PREVIEW); + + m_nHoverRow = -1; + + CHoverPreviewWnd::HideIfVisible(); + + SuppressStandardToolTip(false); +} + +// Turns the normal tooltip bubble of the list off and on again, it is not +// needed while the hover preview popup shows the same clip. +void CQListCtrl::SuppressStandardToolTip(bool bSuppress) +{ + if (::IsWindow(m_hwndStdToolTip) == FALSE) + { + m_bStdToolTipSuppressed = false; + return; + } + + if (bSuppress == m_bStdToolTipSuppressed) + { + return; + } + + if (bSuppress) + { + ::SendMessage(m_hwndStdToolTip, TTM_POP, 0, 0); + ::SendMessage(m_hwndStdToolTip, TTM_ACTIVATE, FALSE, 0); + } + else + { + ::SendMessage(m_hwndStdToolTip, TTM_ACTIVATE, TRUE, 0); + } + + m_bStdToolTipSuppressed = bSuppress; } bool CQListCtrl::MouseInScrollBarArea(CRect crWindow, CPoint point) @@ -2184,6 +2471,8 @@ void CQListCtrl::SetSearchText(CString text) void CQListCtrl::HidePopup(bool checkShowPersistant) { + StopHoverPreview(); + if (VALID_TOOLTIP) { if (checkShowPersistant == false || @@ -2314,6 +2603,8 @@ void CQListCtrl::CreateSmallFont() void CQListCtrl::OnMouseHWheel(UINT nFlags, short zDelta, CPoint pt) { + StopHoverPreview(); + if (zDelta < 0) { this->SendMessage(WM_HSCROLL, SB_LINERIGHT, NULL); diff --git a/src/QListCtrl.h b/src/QListCtrl.h index 01ef076d..cbec0a87 100644 --- a/src/QListCtrl.h +++ b/src/QListCtrl.h @@ -134,6 +134,9 @@ class CQListCtrl : public CListCtrl BOOL SetItemCountEx(int iCount, DWORD dwFlags = 0); void HidePopup(bool checkShowPersistant); + + // hides the hover preview popup, see HoverPreviewWnd.h + void StopHoverPreview(); void ToggleToolTipShowPersistant(); bool ToggleToolTipWordWrap(); void SetTooltipActions(CAccels *pToolTipActions) { m_pToolTipActions = pToolTipActions; } @@ -175,6 +178,13 @@ class CQListCtrl : public CListCtrl COLORREF HslToRgb(double h, double s, double l); void DrawCheckerboard(CDC* pDC, CRect rect); + + // hover preview popup of the row under the mouse pointer + void TrackHover(CPoint point); + int GetRowFromPoint(CPoint point); + void ShowHoverPreview(int nRow); + void SuppressStandardToolTip(bool bSuppress); + WCHAR *m_pwchTip; TCHAR *m_pchTip; HFONT m_SmallFont; @@ -200,6 +210,10 @@ class CQListCtrl : public CListCtrl CAccels *m_pToolTipActions; CRichEditCtrlEx m_rtfFormater; CDPI *m_windowDpi; + int m_nHoverRow; + bool m_bTrackingMouse; + HWND m_hwndStdToolTip; + bool m_bStdToolTipSuppressed; // Generated message map functions @@ -207,6 +221,9 @@ class CQListCtrl : public CListCtrl //{{AFX_MSG(CQListCtrl) afx_msg void OnKeydown(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnCustomdrawList(NMHDR* pNMHDR, LRESULT* pResult); + // colour of a colour code clip text, shared by the swatch in the row and + // the one in the hover preview popup + bool ParseColorCode(const CString& csText, COLORREF& crColor, int& nAlpha, CString& csCleanedText); void DrawCopiedColorCode(CString& csText, CRect& rcText, CDC* pDC); afx_msg void OnSysKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg BOOL OnEraseBkgnd(CDC* pDC); @@ -219,6 +236,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..53008fff 100644 --- a/src/QPasteWnd.cpp +++ b/src/QPasteWnd.cpp @@ -319,6 +319,8 @@ BEGIN_MESSAGE_MAP(CQPasteWnd, CWndEx) ON_UPDATE_COMMAND_UI(32775, &CQPasteWnd::OnUpdate32775) ON_COMMAND_RANGE(CustomFriendStartId, (CustomFriendStartId + MaxCustomFriends + 1), OnCustomSendToFriend) ON_COMMAND_RANGE(ChaiScriptMenuStartId, (ChaiScriptMenuStartId + MaxChaiScripts + 1), OnChaiScriptPaste) + ON_COMMAND_RANGE(ID_MENU_HOVER_PREVIEW_FIRST, ID_MENU_HOVER_PREVIEW_LAST, OnHoverPreviewWidth) + ON_COMMAND(ID_MENU_HOVER_PREVIEW_TOGGLE, OnHoverPreviewToggle) ON_MESSAGE(WM_DPICHANGED, OnDpiChanged) ON_COMMAND(ID_CLIPORDER_MOVETOLAST, &CQPasteWnd::OnCliporderMovetolast) ON_UPDATE_COMMAND_UI(ID_CLIPORDER_MOVETOLAST, &CQPasteWnd::OnUpdateCliporderMovetolast) @@ -1690,6 +1692,55 @@ BOOL CQPasteWnd::FillList(CString csSQLSearch) return TRUE; } +// Width in pixels of the hover preview popup of the quick paste list. The +// quick paste menu items ID_MENU_HOVER_PREVIEW_FIRST to +// ID_MENU_HOVER_PREVIEW_LAST are in this order, 0 shows the image in its +// original size. +static const int g_hoverPreviewWidths[] = +{ + 0, + 200, + 300, + 400, + 600, + 800 +}; + +void CQPasteWnd::OnHoverPreviewWidth(UINT nID) +{ + int nIndex = (int)nID - (int)ID_MENU_HOVER_PREVIEW_FIRST; + + if (nIndex < 0 || nIndex >= _countof(g_hoverPreviewWidths)) + { + return; + } + + CGetSetOptions::SetToolTipPreviewWidth(g_hoverPreviewWidths[nIndex]); +} + +void CQPasteWnd::OnHoverPreviewToggle() +{ + CGetSetOptions::SetToolTipPreviewEnabled(CGetSetOptions::GetToolTipPreviewEnabled() ? FALSE : TRUE); +} + +void CQPasteWnd::SetHoverPreviewWidthChecks(CMenu* pMenu) +{ + if (CGetSetOptions::GetToolTipPreviewEnabled()) + { + pMenu->CheckMenuItem(ID_MENU_HOVER_PREVIEW_TOGGLE, MF_CHECKED); + } + + int nCurrent = CGetSetOptions::GetToolTipPreviewWidth(); + + for (int nIndex = 0; nIndex < _countof(g_hoverPreviewWidths); nIndex++) + { + if (g_hoverPreviewWidths[nIndex] == nCurrent) + { + pMenu->CheckMenuItem(ID_MENU_HOVER_PREVIEW_FIRST + nIndex, MF_CHECKED); + } + } +} + void CQPasteWnd::ShowRightClickMenu() { POINT pp; @@ -7278,6 +7329,8 @@ void CQPasteWnd::OnSystemButton() SetMenuChecks(cmSubMenu); + SetHoverPreviewWidthChecks(cmSubMenu); + cmSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON, pp.x, pp.y, this, NULL); } } diff --git a/src/QPasteWnd.h b/src/QPasteWnd.h index 2a42b5ec..8a4d9898 100644 --- a/src/QPasteWnd.h +++ b/src/QPasteWnd.h @@ -205,6 +205,7 @@ class CQPasteWnd: public CWndEx void SetMenuChecks(CMenu *pMenu); void SetSendToMenu(CMenu *pMenu, int nMenuID, int nArrayPos); void SetFriendChecks(CMenu *pMenu); + void SetHoverPreviewWidthChecks(CMenu *pMenu); BOOL SendToFriendbyPos(int nPos, CString override_IP_Host); @@ -345,6 +346,9 @@ class CQPasteWnd: public CWndEx void ShowRightClickMenu(); + afx_msg void OnHoverPreviewWidth(UINT nID); + afx_msg void OnHoverPreviewToggle(); + void SetCurrentTransparency(); // Generated message map functions