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