diff --git a/CP_Main.rc b/CP_Main.rc
index 5a41e2b4..3d5e84a8 100644
--- a/CP_Main.rc
+++ b/CP_Main.rc
@@ -1247,6 +1247,26 @@ BEGIN
PUSHBUTTON "Reset",IDC_BUTTON_RESET,318,7,50,14
END
+IDD_OPTIONS_CLIPBOARD_CAROUSEL DIALOGEX 0, 0, 370, 286
+STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_CAPTION
+CAPTION "Clipboard Carousel"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ CONTROL "Enable Clipboard Carousel",IDC_CC_ENABLE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,8,8,225,10
+ GROUPBOX "Activate carousel",IDC_CC_EDITOR_GROUP,8,29,351,93
+ CONTROL "Keyboard shortcut",IDC_CC_KEYBOARD,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,17,43,93,10
+ CONTROL "Mouse button + modifiers",IDC_CC_MOUSE,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,120,43,218,10
+ CONTROL "",IDC_CC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,17,63,246,14
+ COMBOBOX IDC_CC_MOUSE_TRIGGER,17,63,246,70,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
+ LTEXT "Mouse modifiers (optional):",IDC_CC_MODIFIERS_LABEL,17,84,326,8
+ CONTROL "Shift",IDC_CC_SHIFT,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,17,99,60,10
+ CONTROL "Ctrl",IDC_CC_CONTROL,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,86,99,60,10
+ CONTROL "Alt",IDC_CC_ALT,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,155,99,60,10
+ PUSHBUTTON "Clear",IDC_CC_CLEAR,287,63,60,14
+ CONTROL "Paste when clicking outside the carousel",IDC_CC_PASTE_CLICK,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,8,132,345,10
+ LTEXT "",IDC_CC_HELP,8,156,345,91
+END
+
IDD_DIALOG_FREIND_PROMPT DIALOGEX 0, 0, 237, 91
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Send to Friend"
diff --git a/CP_Main.vcxproj b/CP_Main.vcxproj
index cd39b313..940b33fa 100644
--- a/CP_Main.vcxproj
+++ b/CP_Main.vcxproj
@@ -541,6 +541,9 @@
+
+
+
@@ -618,6 +621,7 @@
+
@@ -809,6 +813,10 @@
+
+
+
+
@@ -886,6 +894,7 @@
+
@@ -1267,4 +1276,4 @@
-
\ No newline at end of file
+
diff --git a/CP_Main.vcxproj.filters b/CP_Main.vcxproj.filters
index ba9944ba..9240224c 100644
--- a/CP_Main.vcxproj.filters
+++ b/CP_Main.vcxproj.filters
@@ -46,6 +46,15 @@
src
+
+ src
+
+
+ src
+
+
+ src
+
src
@@ -244,6 +253,9 @@
src
+
+ src
+
src
@@ -541,6 +553,18 @@
src
+
+ src
+
+
+ src
+
+
+ src
+
+
+ src
+
src
@@ -760,6 +784,9 @@
src
+
+ src
+
src
@@ -1684,4 +1711,4 @@
-
\ No newline at end of file
+
diff --git a/resource.h b/resource.h
index 6ecab036..c9492a2b 100644
--- a/resource.h
+++ b/resource.h
@@ -255,6 +255,20 @@
#define IDR_MENU2 390
#define IDR_MENU_DELETE_CLIP_DATA 390
#define IDR_MENU3 391
+#define IDD_OPTIONS_CLIPBOARD_CAROUSEL 394
+#define IDC_CC_ENABLE 2200
+#define IDC_CC_EDITOR_GROUP 2201
+#define IDC_CC_KEYBOARD 2202
+#define IDC_CC_MOUSE 2203
+#define IDC_CC_HOTKEY 2204
+#define IDC_CC_MOUSE_TRIGGER 2205
+#define IDC_CC_SHIFT 2206
+#define IDC_CC_CONTROL 2207
+#define IDC_CC_ALT 2208
+#define IDC_CC_CLEAR 2209
+#define IDC_CC_PASTE_CLICK 2210
+#define IDC_CC_HELP 2211
+#define IDC_CC_MODIFIERS_LABEL 2212
#define IDC_PATH 1000
#define IDC_GET_PATH 1001
#define IDC_SELECT_SOUND 1002
@@ -882,9 +896,9 @@
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_3D_CONTROLS 1
-#define _APS_NEXT_RESOURCE_VALUE 394
+#define _APS_NEXT_RESOURCE_VALUE 395
#define _APS_NEXT_COMMAND_VALUE 33002
-#define _APS_NEXT_CONTROL_VALUE 2174
+#define _APS_NEXT_CONTROL_VALUE 2213
#define _APS_NEXT_SYMED_VALUE 104
#endif
#endif
diff --git a/src/ClipboardCarousel.cpp b/src/ClipboardCarousel.cpp
new file mode 100644
index 00000000..acfc6566
--- /dev/null
+++ b/src/ClipboardCarousel.cpp
@@ -0,0 +1,589 @@
+#include "stdafx.h"
+#include "CP_Main.h"
+#include "ClipboardCarousel.h"
+#include "Options.h"
+#include "ProcessPaste.h"
+#include "Clip.h"
+#include "Misc.h"
+#include "ImageHelper.h"
+#include
+#include
+
+namespace
+{
+ const int BATCH_SIZE = 100;
+ const size_t CACHE_SIZE = 300;
+
+ bool IsDesktopTarget(HWND window)
+ {
+ TCHAR className[128] = {};
+ GetClassName(window,className,_countof(className));
+ CString name(className);
+ return window == GetDesktopWindow() || name == _T("Progman") || name == _T("WorkerW");
+ }
+
+ bool IsShellWindow(HWND window)
+ {
+ if(IsDesktopTarget(window)) return true;
+ TCHAR className[128] = {};
+ GetClassName(window,className,_countof(className));
+ CString name(className);
+ return name == _T("Shell_TrayWnd") || name == _T("Shell_SecondaryTrayWnd");
+ }
+}
+
+CClipboardCarousel::CClipboardCarousel()
+ : m_owner(NULL), m_notifyWnd(NULL), m_active(false), m_shutdown(false), m_clickPending(false),
+ m_target(NULL), m_focus(NULL), m_targetProcess(0), m_clickFocus(NULL), m_clickDeadline(0),
+ m_emptyUntil(0), m_previewGeneration(0), m_snapshotOrder(0), m_snapshotId(0), m_current(0),
+ m_stop(false), m_requestPending(false)
+{
+}
+CClipboardCarousel::~CClipboardCarousel() { Shutdown(); }
+bool CClipboardCarousel::Initialize(CWnd* owner)
+{
+ m_owner = owner;
+ m_notifyWnd = owner->GetSafeHwnd();
+ if(!m_window.Create(owner)) return false;
+ return m_input.Initialize(m_notifyWnd,m_window.GetSafeHwnd());
+}
+void CClipboardCarousel::Shutdown()
+{
+ if(m_shutdown) return;
+ m_shutdown = true;
+ Cancel(false);
+ m_input.Shutdown();
+ StopWorker();
+ MSG message;
+ while(PeekMessage(&message,m_notifyWnd,WM_CLIPBOARD_CAROUSEL_PREVIEW_READY,
+ WM_CLIPBOARD_CAROUSEL_PREVIEW_READY,PM_REMOVE))
+ HandlePreviewResult((ClipboardCarouselPreviewResult*)message.wParam);
+ if(m_window.GetSafeHwnd()) m_window.DestroyWindow();
+ m_notifyWnd = NULL;
+ m_owner = NULL;
+}
+void CClipboardCarousel::SuspendInput()
+{
+ Cancel();
+ m_input.Suspend();
+ StopWorker();
+}
+void CClipboardCarousel::ResumeInput()
+{
+ CString error;
+ if(!m_input.Resume(error) && !error.IsEmpty())
+ m_owner->MessageBox(error,theApp.m_Language.GetString("ClipboardCarouselTitle","Clipboard Carousel"),MB_OK | MB_ICONWARNING);
+ if(!m_input.IsEnabled()) StopWorker();
+}
+void CClipboardCarousel::HandleCommand(ClipboardCarouselCommand command, UINT_PTR generation)
+{
+ if(command == ClipboardCarouselCommand::Activate)
+ {
+ if(!m_active) Start();
+ return;
+ }
+ if(!m_active || generation != m_input.Generation()) return;
+ switch(command)
+ {
+ case ClipboardCarouselCommand::Older: if(!m_clickPending) Move(true); break;
+ case ClipboardCarouselCommand::Newer: if(!m_clickPending) Move(false); break;
+ case ClipboardCarouselCommand::Confirm: if(!m_clickPending) Confirm(); break;
+ case ClipboardCarouselCommand::Cancel: Cancel(); break;
+ case ClipboardCarouselCommand::ClickCancel: Cancel(false); break;
+ case ClipboardCarouselCommand::ClickPaste:
+ if(!CGetSetOptions::GetCarouselPasteAtClick()) Cancel(false);
+ else if(!m_clickPending)
+ {
+ m_clickPending = true;
+ m_clickFocus = NULL;
+ m_clickDeadline = GetTickCount64() + 1000;
+ }
+ break;
+ default: break;
+ }
+}
+void CClipboardCarousel::Start()
+{
+ if(m_shutdown || !m_input.IsEnabled() || (theApp.QPasteWnd() && IsWindowVisible(theApp.QPasteWnd()->GetSafeHwnd())))
+ return;
+ if(!theApp.m_activeWnd.TrackActiveWnd(true)) return;
+ m_target = theApp.m_activeWnd.ActiveWnd();
+ m_focus = theApp.m_activeWnd.FocusWnd();
+ GetWindowThreadProcessId(m_target,&m_targetProcess);
+ m_window.RefreshColors();
+ m_items.clear();
+ m_current = 0;
+ if(!LoadBatch(true,true))
+ {
+ m_window.SetPreview(theApp.m_Language.GetString("ClipboardCarouselEmptyHistory","Clipboard history is empty."));
+ m_window.ShowPreview();
+ m_emptyUntil = GetTickCount64() + 1000;
+ return;
+ }
+ CString error;
+ if(!m_input.BeginSession(error))
+ {
+ m_owner->MessageBox(error,theApp.m_Language.GetString("ClipboardCarouselTitle","Clipboard Carousel"),MB_OK | MB_ICONWARNING);
+ m_items.clear();
+ return;
+ }
+ m_active = true;
+ m_emptyUntil = 0;
+ RefreshPreview();
+}
+bool CClipboardCarousel::LoadBatch(bool older, bool initial, bool restart)
+{
+ try
+ {
+ if(initial)
+ {
+ CppSQLite3Query top = theApp.m_db.execQuery(_T("SELECT clipOrder FROM Main INDEXED BY Main_ClipOrder WHERE bIsGroup = 0 ORDER BY clipOrder DESC, lID DESC LIMIT 1"));
+ if(top.eof()) return false;
+ m_snapshotOrder = top.getFloatField(0);
+ m_snapshotId = theApp.m_db.execScalar(_T("SELECT MAX(lID) FROM Main"));
+ }
+ CString boundary;
+ if(restart)
+ {
+ m_items.clear();
+ m_current = 0;
+ }
+ if(!initial && !restart && !m_items.empty())
+ {
+ const Item& edge = older ? m_items.back() : m_items.front();
+ boundary = StrF(older ? _T("AND clipOrder <= %.17g AND (clipOrder < %.17g OR (clipOrder = %.17g AND lID < %d))") :
+ _T("AND clipOrder >= %.17g AND (clipOrder > %.17g OR (clipOrder = %.17g AND lID > %d))"),edge.order,edge.order,edge.order,edge.id);
+ }
+ CppSQLite3Query query = theApp.m_db.execQueryEx(
+ _T("SELECT lID, clipOrder, substr(mText,1,4096) FROM Main INDEXED BY Main_ClipOrder WHERE bIsGroup = 0 AND clipOrder <= %.17g AND lID <= %d %s ORDER BY clipOrder %s, lID %s LIMIT %d"),
+ m_snapshotOrder,m_snapshotId,(LPCTSTR)boundary,older ? _T("DESC") : _T("ASC"),older ? _T("DESC") : _T("ASC"),BATCH_SIZE);
+ size_t added = 0;
+ while(!query.eof())
+ {
+ Item item = { query.getIntField(0),query.getFloatField(1),query.getStringField(2) };
+ if(older) m_items.push_back(item);
+ else m_items.push_front(item);
+ added++;
+ query.nextRow();
+ }
+ if(restart && added > 0)
+ m_current = older ? 0 : m_items.size()-1;
+ else if(!older)
+ m_current += added;
+ while(m_items.size() > CACHE_SIZE)
+ {
+ if(older) { m_items.pop_front(); m_current--; }
+ else m_items.pop_back();
+ }
+ return added > 0;
+ }
+ CATCH_SQLITE_EXCEPTION
+ return false;
+}
+bool CClipboardCarousel::ClipExists(int id)
+{
+ try { return theApp.m_db.execScalar(StrF(_T("SELECT EXISTS(SELECT 1 FROM Main WHERE lID = %d AND bIsGroup = 0)"),id)) != 0; }
+ CATCH_SQLITE_EXCEPTION
+ return false;
+}
+void CClipboardCarousel::Move(bool older)
+{
+ bool wrapped = false;
+ if(older)
+ {
+ if(m_current + 1 >= m_items.size() && !LoadBatch(true))
+ {
+ if(!LoadBatch(true,false,true)) { Cancel(); return; }
+ wrapped = true;
+ }
+ if(!wrapped) m_current++;
+ }
+ else
+ {
+ if(m_current == 0 && !LoadBatch(false))
+ {
+ if(!LoadBatch(false,false,true)) { Cancel(); return; }
+ wrapped = true;
+ }
+ if(!wrapped) m_current--;
+ }
+ while(!m_items.empty() && !ClipExists(m_items[m_current].id))
+ {
+ m_items.erase(m_items.begin() + m_current);
+ if(m_items.empty()) { Cancel(); return; }
+ if(!older && m_current > 0) m_current--;
+ if(m_current >= m_items.size())
+ {
+ if(!LoadBatch(true)) m_current = m_items.size()-1;
+ }
+ }
+ RefreshPreview();
+}
+void CClipboardCarousel::RefreshPreview()
+{
+ if(m_items.empty()) return;
+ const Item& item = m_items[m_current];
+ CString text = item.description;
+ bool image = false, files = false;
+ try
+ {
+ CppSQLite3Query types = theApp.m_db.execQueryEx(_T("SELECT strClipboardFormat FROM Data WHERE lParentID = %d"),item.id);
+ while(!types.eof())
+ {
+ CString type = types.getStringField(0);
+ image |= type == GetFormatName(CF_DIB) || type.CompareNoCase(_T("PNG")) == 0;
+ files |= type == GetFormatName(CF_HDROP);
+ types.nextRow();
+ }
+ // Only a bounded text prefix is needed for selection and preview.
+ CppSQLite3Query plain = theApp.m_db.execQueryEx(
+ _T("SELECT strClipboardFormat, substr(ooData,1,8192) FROM Data WHERE lParentID = %d AND strClipboardFormat IN ('CF_UNICODETEXT','CF_TEXT') ORDER BY CASE strClipboardFormat WHEN 'CF_UNICODETEXT' THEN 0 ELSE 1 END LIMIT 1"),item.id);
+ if(!plain.eof())
+ {
+ int length = 0;
+ const unsigned char* data = plain.getBlobField(1,length);
+ if(data && length > 0)
+ {
+ if(CString(plain.getStringField(0)) == GetFormatName(CF_UNICODETEXT))
+ text = CString((LPCWSTR)data,length / sizeof(WCHAR));
+ else
+ text = CString(CStringA((LPCSTR)data,length));
+ int end = text.Find(_T('\0'));
+ if(end >= 0) text = text.Left(end);
+ }
+ }
+ }
+ CATCH_SQLITE_EXCEPTION
+ if(files)
+ {
+ image = false;
+ HGLOBAL data = CClip::LoadFormat(item.id,CF_HDROP);
+ if(data)
+ {
+ CClipFormat format(CF_HDROP,data);
+ text.Empty();
+ UINT count = DragQueryFile((HDROP)data,0xFFFFFFFF,NULL,0);
+ for(UINT i = 0; i < count && text.GetLength() < 4096; i++)
+ {
+ CString path;
+ UINT length = DragQueryFile((HDROP)data,i,NULL,0);
+ DragQueryFile((HDROP)data,i,path.GetBuffer(min(length,4096U)+1),min(length,4096U)+1);
+ path.ReleaseBuffer();
+ if(i) text += _T("\r\n");
+ text += path;
+ }
+ }
+ }
+ if(text.IsEmpty()) text = theApp.m_Language.GetString("ClipboardCarouselNoPreview","No preview available.");
+ m_previewGeneration++;
+ m_window.SetPreview(text,image);
+ m_window.ShowPreview();
+ if(image)
+ {
+ StartWorker();
+ {
+ std::lock_guard lock(m_mutex);
+ m_request = { item.id,m_previewGeneration,m_window.BackgroundColor() };
+ m_requestPending = true;
+ }
+ m_condition.notify_one();
+ }
+}
+void CClipboardCarousel::Confirm(bool clicked)
+{
+ if(!m_active || m_items.empty()) return;
+ // Desktop activation is useful for browsing, but Enter must not paste files
+ // into Explorer's desktop. A destination click establishes an application target.
+ if(IsShellWindow(m_target))
+ {
+ Cancel();
+ return;
+ }
+ int id = m_items[m_current].id;
+ if(!ClipExists(id)) { Move(true); return; }
+ DWORD process = 0;
+ GetWindowThreadProcessId(m_target,&process);
+ if(!IsWindow(m_target) || process != m_targetProcess ||
+ !theApp.m_activeWnd.RestoreTrackedWnd(m_target,m_focus))
+ {
+ Cancel(false);
+ return;
+ }
+ CString selected = m_window.SelectedText();
+ if(!selected.IsEmpty())
+ {
+ HGLOBAL data = NewGlobalP((LPVOID)(LPCTSTR)selected,(selected.GetLength()+1)*sizeof(TCHAR));
+ HGLOBAL ignore = NewGlobalP("Ignore",sizeof("Ignore"));
+ if(!data || !ignore)
+ {
+ if(data) GlobalFree(data);
+ if(ignore) GlobalFree(ignore);
+ return;
+ }
+ EndSession();
+ COleDataSource* source = new COleDataSource;
+ source->CacheGlobalData(CF_UNICODETEXT,data);
+ source->CacheGlobalData(theApp.m_cfIgnoreClipboard,ignore);
+ source->SetClipboard();
+ if(clicked || CGetSetOptions::m_bSendPasteMessageAfterSelection)
+ theApp.m_activeWnd.SendPaste(!clicked);
+ else
+ theApp.m_activeWnd.ActivateTarget();
+ return;
+ }
+ EndSession();
+ CProcessPaste paste;
+ paste.GetClipIDs().Add(id);
+ paste.m_bSendPaste = clicked || CGetSetOptions::m_bSendPasteMessageAfterSelection;
+ paste.m_bActivateTarget = !clicked;
+ if(paste.DoPaste()) theApp.OnPasteCompleted();
+ else Log(_T("Clipboard Carousel paste failed: ") + paste.m_lastErrorMessage);
+}
+void CClipboardCarousel::EndSession()
+{
+ m_active = false;
+ m_previewGeneration++;
+ m_input.EndSession();
+ m_window.HidePreview();
+ m_items.clear();
+ m_clickPending = false;
+ m_emptyUntil = 0;
+ std::lock_guard lock(m_mutex);
+ m_requestPending = false;
+}
+void CClipboardCarousel::Cancel(bool restoreFocus)
+{
+ bool restore = restoreFocus && m_active && m_window.HasFocus();
+ EndSession();
+ if(restore && theApp.m_activeWnd.RestoreTrackedWnd(m_target,m_focus))
+ theApp.m_activeWnd.ActivateTarget();
+}
+void CClipboardCarousel::PasteAtClick()
+{
+ HWND clicked = m_input.ClickedWindow();
+ HWND root = GetAncestor(clicked,GA_ROOT);
+ if(!IsWindow(clicked) || GetTickCount64() >= m_clickDeadline)
+ {
+ Cancel(false);
+ return;
+ }
+ bool desktopClick = IsDesktopTarget(root);
+ HWND foreground = GetForegroundWindow();
+ // The desktop can have different Progman/WorkerW roots across monitors.
+ if(foreground != root && !(desktopClick && IsDesktopTarget(foreground))) return;
+ POINT point = m_input.ClickedPoint();
+ DWORD_PTR hit = 0;
+ if(!SendMessageTimeout(root,WM_NCHITTEST,0,MAKELPARAM(point.x,point.y),
+ SMTO_ABORTIFHUNG,50,&hit) || hit != HTCLIENT)
+ {
+ Cancel(false);
+ return;
+ }
+ if(GetAsyncKeyState(VK_LBUTTON) & 0x8000 || GetAsyncKeyState(VK_CONTROL) & 0x8000 ||
+ GetAsyncKeyState(VK_SHIFT) & 0x8000 || GetAsyncKeyState(VK_MENU) & 0x8000 ||
+ (GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000)
+ return;
+ if(IsShellWindow(root))
+ {
+ if(desktopClick && IsDesktopTarget(m_target) && m_window.IsImagePreview())
+ SaveDesktopImage();
+ else
+ Cancel(false);
+ return;
+ }
+ if(!theApp.m_activeWnd.TrackActiveWnd(true) || theApp.m_activeWnd.DesktopHasFocus() ||
+ theApp.m_activeWnd.ActiveWnd() != root)
+ {
+ Cancel(false);
+ return;
+ }
+ HWND focus = theApp.m_activeWnd.FocusWnd();
+ if(m_clickFocus != focus) { m_clickFocus = focus; return; }
+ m_target = root;
+ m_focus = focus;
+ GetWindowThreadProcessId(root,&m_targetProcess);
+ Confirm(true);
+}
+
+void CClipboardCarousel::SaveDesktopImage()
+{
+ m_clickPending = false;
+ if(m_items.empty() || !ClipExists(m_items[m_current].id))
+ {
+ Cancel(false);
+ return;
+ }
+ bool saved = false;
+ CString savedPath;
+ do
+ {
+ PWSTR desktop = NULL;
+ HRESULT result = SHGetKnownFolderPath(FOLDERID_Desktop,KF_FLAG_DEFAULT,NULL,&desktop);
+ CString folder = desktop ? desktop : _T("");
+ CoTaskMemFree(desktop);
+ if(FAILED(result) || folder.IsEmpty()) break;
+
+ // Decode the original saved format, never the downscaled preview bitmap.
+ CClipFormat format(theApp.m_PNG_Format);
+ if(!theApp.GetClipData(m_items[m_current].id,format))
+ {
+ format.Free(); format.Clear();
+ format.m_cfType = CF_DIB;
+ theApp.GetClipData(m_items[m_current].id,format);
+ }
+ if(!format.m_hgData) break;
+ std::shared_ptr image = format.m_cfType == theApp.m_PNG_Format ?
+ PNGImageHelper::CImageFromHGLOBAL(format.m_hgData) :
+ DIBImageHelper::CImageFromHGLOBAL(format.m_hgData);
+ if(!image || image->IsNull()) break;
+ CComPtr encoded;
+ if(FAILED(CreateStreamOnHGlobal(NULL,TRUE,&encoded)) ||
+ FAILED(image->Save(encoded,Gdiplus::ImageFormatPNG))) break;
+ STATSTG info = {};
+ HGLOBAL buffer = NULL;
+ if(FAILED(encoded->Stat(&info,STATFLAG_NONAME)) || info.cbSize.HighPart != 0 ||
+ info.cbSize.LowPart == 0 || FAILED(GetHGlobalFromStream(encoded,&buffer))) break;
+ BYTE* bytes = (BYTE*)GlobalLock(buffer);
+ if(!bytes) break;
+
+ SYSTEMTIME time = {};
+ GetLocalTime(&time);
+ CString stem = StrF(_T("Ditto image %04d-%02d-%02d %02d-%02d-%02d"),
+ time.wYear,time.wMonth,time.wDay,time.wHour,time.wMinute,time.wSecond);
+ for(int suffix = 0; suffix < 1000; suffix++)
+ {
+ CString name = stem + (suffix ? StrF(_T(" (%d)"),suffix+1) : CString()) + _T(".png");
+ savedPath = folder + _T("\\") + name;
+ HANDLE handle = CreateFile(savedPath,GENERIC_WRITE | DELETE,0,NULL,
+ CREATE_NEW,FILE_ATTRIBUTE_NORMAL,NULL);
+ if(handle == INVALID_HANDLE_VALUE)
+ {
+ DWORD error = GetLastError();
+ if(error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) continue;
+ break;
+ }
+ CHandle file(handle);
+ DWORD offset = 0;
+ bool written = true;
+ while(offset < info.cbSize.LowPart)
+ {
+ DWORD count = min(info.cbSize.LowPart-offset,1024UL*1024);
+ DWORD actual = 0;
+ if(!WriteFile(file,bytes+offset,count,&actual,NULL) || actual == 0)
+ {
+ written = false;
+ break;
+ }
+ offset += actual;
+ }
+ saved = written && FlushFileBuffers(file) != FALSE;
+ if(!saved)
+ {
+ // Remove only the failed new file, through its exclusive handle.
+ FILE_DISPOSITION_INFO discard = { TRUE };
+ SetFileInformationByHandle(file,FileDispositionInfo,&discard,sizeof(discard));
+ }
+ break;
+ }
+ GlobalUnlock(buffer);
+ } while(false);
+ // Export does not load the clipboard or mark/reorder the original history clip.
+ EndSession();
+ if(saved)
+ SHChangeNotify(SHCNE_CREATE,SHCNF_PATH,(LPCTSTR)savedPath,NULL);
+ else
+ m_owner->MessageBox(theApp.m_Language.GetString("ClipboardCarouselDesktopSaveFailed",
+ "Unable to save the image to the Desktop. Check that the image is valid and the Desktop folder is writable."),
+ theApp.m_Language.GetString("ClipboardCarouselTitle","Clipboard Carousel"),MB_OK | MB_ICONWARNING);
+}
+void CClipboardCarousel::Tick()
+{
+ m_input.Tick();
+ if(m_emptyUntil && GetTickCount64() >= m_emptyUntil)
+ {
+ m_emptyUntil = 0;
+ m_window.HidePreview();
+ }
+ if(m_active && m_clickPending) PasteAtClick();
+}
+void CClipboardCarousel::BeforeDatabaseClose() { Cancel(); StopWorker(); }
+void CClipboardCarousel::StartWorker()
+{
+ if(!m_worker.joinable())
+ {
+ m_stop = false;
+ m_worker = std::thread(&CClipboardCarousel::PreviewWorker,this);
+ }
+}
+void CClipboardCarousel::StopWorker()
+{
+ {
+ std::lock_guard lock(m_mutex);
+ m_stop = true;
+ m_requestPending = false;
+ }
+ m_condition.notify_one();
+ if(m_worker.joinable()) m_worker.join();
+}
+void CClipboardCarousel::PreviewWorker()
+{
+ while(true)
+ {
+ PreviewRequest request;
+ {
+ std::unique_lock lock(m_mutex);
+ m_condition.wait(lock,[&]() { return m_stop || m_requestPending; });
+ if(m_stop) return;
+ request = m_request;
+ m_requestPending = false;
+ }
+ CClipFormat format(CF_DIB);
+ if(!theApp.GetClipData(request.id,format))
+ {
+ format.Free(); format.Clear();
+ format.m_cfType = theApp.m_PNG_Format;
+ theApp.GetClipData(request.id,format);
+ }
+ HBITMAP image = NULL;
+ if(format.m_hgData)
+ {
+ Gdiplus::Bitmap* original = format.CreateGdiplusBitmap();
+ if(original && original->GetWidth() && original->GetHeight())
+ {
+ double scale = min(1.0,min(720.0/original->GetWidth(),420.0/original->GetHeight()));
+ INT width = max(1,(INT)(original->GetWidth()*scale));
+ INT height = max(1,(INT)(original->GetHeight()*scale));
+ Gdiplus::Bitmap thumbnail(width,height,PixelFormat32bppPARGB);
+ {
+ Gdiplus::Graphics graphics(&thumbnail);
+ graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
+ graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
+ graphics.DrawImage(original,0,0,width,height);
+ }
+ thumbnail.GetHBITMAP(Gdiplus::Color(255,GetRValue(request.background),
+ GetGValue(request.background),GetBValue(request.background)),&image);
+ }
+ delete original;
+ }
+ ClipboardCarouselPreviewResult* result = new ClipboardCarouselPreviewResult;
+ result->generation = request.generation;
+ result->clipId = request.id;
+ result->image = image;
+ if(!PostMessage(m_notifyWnd,WM_CLIPBOARD_CAROUSEL_PREVIEW_READY,(WPARAM)result,0))
+ {
+ if(image) DeleteObject(image);
+ delete result;
+ }
+ }
+}
+void CClipboardCarousel::HandlePreviewResult(ClipboardCarouselPreviewResult* result)
+{
+ if(!result) return;
+ if(m_active && !m_items.empty() && result->generation == m_previewGeneration && result->clipId == m_items[m_current].id)
+ {
+ m_window.SetImage(result->image);
+ result->image = NULL;
+ }
+ if(result->image) DeleteObject(result->image);
+ delete result;
+}
diff --git a/src/ClipboardCarousel.h b/src/ClipboardCarousel.h
new file mode 100644
index 00000000..87aed4a1
--- /dev/null
+++ b/src/ClipboardCarousel.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "ClipboardCarouselInput.h"
+#include "ClipboardCarouselWnd.h"
+#include
+#include
+#include
+#include
+
+struct ClipboardCarouselPreviewResult
+{
+ ULONGLONG generation = 0;
+ int clipId = -1;
+ HBITMAP image = NULL;
+};
+
+class CClipboardCarousel
+{
+public:
+ CClipboardCarousel();
+ ~CClipboardCarousel();
+ bool Initialize(CWnd* owner);
+ void Shutdown();
+ void SuspendInput();
+ void ResumeInput();
+ bool HandleHotKey(WPARAM id) { return m_input.HandleHotKey(id); }
+ void HandleCommand(ClipboardCarouselCommand command, UINT_PTR generation);
+ void HandlePreviewResult(ClipboardCarouselPreviewResult* result);
+ void Tick();
+ void Cancel(bool restoreFocus = true);
+ void BeforeDatabaseClose();
+ void RefreshColors() { m_window.RefreshColors(); }
+ bool IsActive() const { return m_active; }
+ bool IsEnabled() const { return m_input.IsEnabled(); }
+
+private:
+ struct Item { int id; double order; CString description; };
+ struct PreviewRequest { int id = -1; ULONGLONG generation = 0; COLORREF background = RGB(32,32,32); };
+ void Start();
+ bool LoadBatch(bool older, bool initial = false, bool restart = false);
+ void Move(bool older);
+ bool ClipExists(int id);
+ void RefreshPreview();
+ void Confirm(bool clicked = false);
+ void EndSession();
+ void PasteAtClick();
+ void SaveDesktopImage();
+ void StartWorker();
+ void StopWorker();
+ void PreviewWorker();
+
+ CWnd* m_owner;
+ HWND m_notifyWnd;
+ CClipboardCarouselInput m_input;
+ CClipboardCarouselWnd m_window;
+ bool m_active;
+ bool m_shutdown;
+ bool m_clickPending;
+ HWND m_target;
+ HWND m_focus;
+ DWORD m_targetProcess;
+ HWND m_clickFocus;
+ ULONGLONG m_clickDeadline;
+ ULONGLONG m_emptyUntil;
+ ULONGLONG m_previewGeneration;
+ double m_snapshotOrder;
+ int m_snapshotId;
+ std::deque- m_items;
+ size_t m_current;
+ std::thread m_worker;
+ std::mutex m_mutex;
+ std::condition_variable m_condition;
+ bool m_stop;
+ bool m_requestPending;
+ PreviewRequest m_request;
+};
diff --git a/src/ClipboardCarouselBinding.h b/src/ClipboardCarouselBinding.h
new file mode 100644
index 00000000..63a74fc8
--- /dev/null
+++ b/src/ClipboardCarouselBinding.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "HotKeys.h"
+
+enum class ClipboardCarouselTrigger : BYTE { None, Keyboard, MiddleButton, XButton1, XButton2 };
+enum class ClipboardCarouselCommand : WPARAM { Activate = 1, Older, Newer, Confirm, Cancel, ClickCancel, ClickPaste };
+
+struct ClipboardCarouselBinding
+{
+ ClipboardCarouselTrigger trigger = ClipboardCarouselTrigger::None;
+ BYTE virtualKey = 0;
+ BYTE modifiers = 0;
+
+ bool IsKeyboard() const { return trigger == ClipboardCarouselTrigger::Keyboard; }
+ bool IsMouse() const { return trigger >= ClipboardCarouselTrigger::MiddleButton && trigger <= ClipboardCarouselTrigger::XButton2; }
+ bool IsAssigned() const { return IsMouse() || (IsKeyboard() && virtualKey != 0); }
+ DWORD Encode() const { return (DWORD)trigger | ((DWORD)virtualKey << 8) | ((DWORD)modifiers << 16); }
+ static ClipboardCarouselBinding Decode(DWORD value)
+ {
+ ClipboardCarouselBinding result;
+ result.trigger = (ClipboardCarouselTrigger)(value & 0xFF);
+ result.virtualKey = (BYTE)(value >> 8);
+ result.modifiers = (BYTE)(value >> 16) & (HOTKEYF_SHIFT | HOTKEYF_CONTROL | HOTKEYF_ALT);
+ if(result.trigger > ClipboardCarouselTrigger::XButton2)
+ return ClipboardCarouselBinding();
+ return result;
+ }
+};
+
+#define WM_CLIPBOARD_CAROUSEL_INPUT (WM_APP + 220)
+#define WM_CLIPBOARD_CAROUSEL_PREVIEW_READY (WM_APP + 221)
diff --git a/src/ClipboardCarouselInput.cpp b/src/ClipboardCarouselInput.cpp
new file mode 100644
index 00000000..a1214a84
--- /dev/null
+++ b/src/ClipboardCarouselInput.cpp
@@ -0,0 +1,355 @@
+#include "stdafx.h"
+#include "CP_Main.h"
+#include "ClipboardCarouselInput.h"
+#include "Options.h"
+
+namespace { const UINT HOOK_UPDATE = WM_APP + 222; }
+CClipboardCarouselInput* CClipboardCarouselInput::s_owner = NULL;
+
+CClipboardCarouselInput::CClipboardCarouselInput()
+ : m_owner(NULL), m_preview(NULL), m_atom(0), m_registered(false), m_enabled(false),
+ m_suspended(false), m_active(false), m_generation(0), m_clickedWindow(NULL), m_clickX(0), m_clickY(0),
+ m_swallowedButtons(0), m_swallowedKeyCount(0), m_outsideDown(false), m_downRoot(NULL),
+ m_downGeneration(0), m_wheelRemainder(0), m_thread(NULL), m_threadId(0), m_ready(NULL),
+ m_keyboardWanted(false), m_hooksReady(false), m_mouseHook(NULL), m_keyboardHook(NULL)
+{
+ ZeroMemory(m_swallowedKeys, sizeof(m_swallowedKeys));
+}
+CClipboardCarouselInput::~CClipboardCarouselInput() { Shutdown(); }
+
+bool CClipboardCarouselInput::Initialize(HWND owner, HWND preview)
+{
+ m_owner = owner;
+ m_preview = preview;
+ m_atom = GlobalAddAtom(_T("Ditto_SlimCarousel_Activation"));
+ CString error;
+ return m_atom && Resume(error);
+}
+void CClipboardCarouselInput::UnregisterActivation()
+{
+ if(m_registered) UnregisterHotKey(m_owner,m_atom);
+ m_registered = false;
+}
+void CClipboardCarouselInput::Shutdown()
+{
+ m_enabled = false;
+ EndSession();
+ UnregisterActivation();
+ StopHooks();
+ if(m_atom) GlobalDeleteAtom(m_atom);
+ m_atom = 0;
+ m_owner = m_preview = NULL;
+}
+void CClipboardCarouselInput::Suspend()
+{
+ m_suspended = true;
+ EndSession();
+ UnregisterActivation();
+ StopHooks();
+}
+bool CClipboardCarouselInput::Resume(CString& error)
+{
+ UnregisterActivation();
+ StopHooks();
+ m_binding = ClipboardCarouselBinding::Decode(CGetSetOptions::GetCarouselActivation());
+ m_enabled = CGetSetOptions::GetCarouselEnabled() != FALSE;
+ m_suspended = false;
+ if(!m_enabled) return true;
+ if(!Validate(true,m_binding,error))
+ {
+ m_enabled = false;
+ return false;
+ }
+ if(m_binding.IsKeyboard())
+ {
+ m_registered = RegisterHotKey(m_owner,m_atom,
+ CHotKey::GetModifier(m_binding.modifiers) | MOD_NOREPEAT,m_binding.virtualKey) != FALSE;
+ if(!m_registered)
+ {
+ error = theApp.m_Language.GetString("ClipboardCarouselShortcutUnavailable", "The carousel activation shortcut is unavailable.");
+ m_enabled = false;
+ return false;
+ }
+ return true;
+ }
+ if(UpdateHooks(false,error)) return true;
+ m_enabled = false;
+ return false;
+}
+bool CClipboardCarouselInput::Validate(bool enabled, const ClipboardCarouselBinding& binding, CString& error)
+{
+ if(!enabled) return true;
+ if(!binding.IsAssigned() || (binding.IsKeyboard() &&
+ (binding.virtualKey == VK_CONTROL || binding.virtualKey == VK_SHIFT || binding.virtualKey == VK_MENU ||
+ (binding.modifiers == 0 && (binding.virtualKey < VK_F1 || binding.virtualKey > VK_F24)))))
+ {
+ error = theApp.m_Language.GetString("ClipboardCarouselAssignActivation",
+ "Assign a mouse button, a keyboard shortcut with modifiers, or a function key before enabling Clipboard Carousel.");
+ return false;
+ }
+ // Options suspends our own registration, so this also finds conflicts with other Ditto shortcuts.
+ if(binding.IsKeyboard())
+ {
+ ATOM probe = GlobalAddAtom(_T("Ditto_SlimCarousel_Probe"));
+ bool available = probe && RegisterHotKey(NULL,probe,
+ CHotKey::GetModifier(binding.modifiers) | MOD_NOREPEAT,binding.virtualKey);
+ if(available) UnregisterHotKey(NULL,probe);
+ if(probe) GlobalDeleteAtom(probe);
+ if(!available)
+ {
+ error = theApp.m_Language.GetString("ClipboardCarouselShortcutUnavailable", "The carousel activation shortcut is unavailable.");
+ return false;
+ }
+ }
+ return true;
+}
+bool CClipboardCarouselInput::HandleHotKey(WPARAM id)
+{
+ if(id != m_atom) return false;
+ if(m_enabled && !m_suspended) PostCommand(ClipboardCarouselCommand::Activate);
+ return true;
+}
+bool CClipboardCarouselInput::BeginSession(CString& error)
+{
+ m_wheelRemainder = 0;
+ m_generation++;
+ m_active = true;
+ if(UpdateHooks(true,error)) return true;
+ EndSession();
+ if(m_binding.IsMouse())
+ {
+ CString idleError;
+ if(!UpdateHooks(false,idleError)) m_enabled = false;
+ }
+ return false;
+}
+void CClipboardCarouselInput::EndSession()
+{
+ m_active = false;
+ m_generation++;
+}
+void CClipboardCarouselInput::Tick()
+{
+ // Keep paired key/button releases suppressed before removing a session hook.
+ if(!m_active && m_swallowedButtons == 0 && m_swallowedKeyCount == 0)
+ {
+ if(!m_enabled || m_suspended || !m_binding.IsMouse())
+ StopHooks();
+ else if(m_keyboardWanted)
+ {
+ CString error;
+ UpdateHooks(false,error);
+ }
+ }
+}
+bool CClipboardCarouselInput::UpdateHooks(bool keyboard, CString& error)
+{
+ m_keyboardWanted = keyboard;
+ if(!m_thread)
+ {
+ m_ready = CreateEvent(NULL,TRUE,FALSE,NULL);
+ if(m_ready)
+ {
+ s_owner = this;
+ m_thread = CreateThread(NULL,0,HookThreadProc,this,0,&m_threadId);
+ }
+ }
+ else
+ {
+ ResetEvent(m_ready);
+ PostThreadMessage(m_threadId,HOOK_UPDATE,0,0);
+ }
+ if(m_thread && WaitForSingleObject(m_ready,2000) == WAIT_OBJECT_0 && m_hooksReady)
+ return true;
+ error = theApp.m_Language.GetString("ClipboardCarouselHookInstallError", "Unable to start carousel input handling.");
+ StopHooks();
+ return false;
+}
+void CClipboardCarouselInput::StopHooks()
+{
+ if(m_thread)
+ {
+ if(!PostThreadMessage(m_threadId,WM_QUIT,0,0))
+ {
+ HANDLE startup[] = { m_thread,m_ready };
+ if(WaitForMultipleObjects(2,startup,FALSE,INFINITE) == WAIT_OBJECT_0 + 1)
+ PostThreadMessage(m_threadId,WM_QUIT,0,0);
+ }
+ WaitForSingleObject(m_thread,INFINITE);
+ CloseHandle(m_thread);
+ m_thread = NULL;
+ }
+ if(m_ready) CloseHandle(m_ready);
+ m_ready = NULL;
+ m_threadId = 0;
+ m_hooksReady = false;
+ m_keyboardWanted = false;
+ m_swallowedButtons = 0;
+ m_swallowedKeyCount = 0;
+ ZeroMemory(m_swallowedKeys,sizeof(m_swallowedKeys));
+ m_outsideDown = false;
+ m_wheelRemainder = 0;
+ if(s_owner == this) s_owner = NULL;
+}
+DWORD WINAPI CClipboardCarouselInput::HookThreadProc(LPVOID parameter)
+{
+ return ((CClipboardCarouselInput*)parameter)->HookThread();
+}
+DWORD CClipboardCarouselInput::HookThread()
+{
+ MSG message;
+ PeekMessage(&message,NULL,WM_USER,WM_USER,PM_NOREMOVE);
+ m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL,MouseHookProc,AfxGetInstanceHandle(),0);
+ auto updateKeyboard = [&]()
+ {
+ if(m_keyboardWanted && !m_keyboardHook)
+ m_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL,KeyboardHookProc,AfxGetInstanceHandle(),0);
+ else if(!m_keyboardWanted && m_keyboardHook)
+ {
+ UnhookWindowsHookEx(m_keyboardHook);
+ m_keyboardHook = NULL;
+ }
+ m_hooksReady = m_mouseHook && (!m_keyboardWanted || m_keyboardHook);
+ SetEvent(m_ready);
+ };
+ updateKeyboard();
+ while(m_hooksReady && GetMessage(&message,NULL,0,0) > 0)
+ {
+ if(message.message == HOOK_UPDATE) updateKeyboard();
+ else { TranslateMessage(&message); DispatchMessage(&message); }
+ }
+ if(m_keyboardHook) UnhookWindowsHookEx(m_keyboardHook);
+ if(m_mouseHook) UnhookWindowsHookEx(m_mouseHook);
+ m_mouseHook = m_keyboardHook = NULL;
+ m_hooksReady = false;
+ return 0;
+}
+LRESULT CALLBACK CClipboardCarouselInput::MouseHookProc(int code, WPARAM message, LPARAM data)
+{
+ if(code == HC_ACTION && s_owner && s_owner->MouseEvent(message,*(MSLLHOOKSTRUCT*)data))
+ return 1;
+ return CallNextHookEx(NULL,code,message,data);
+}
+LRESULT CALLBACK CClipboardCarouselInput::KeyboardHookProc(int code, WPARAM message, LPARAM data)
+{
+ if(code == HC_ACTION && s_owner && s_owner->KeyboardEvent(message,*(KBDLLHOOKSTRUCT*)data))
+ return 1;
+ return CallNextHookEx(NULL,code,message,data);
+}
+BYTE CClipboardCarouselInput::Modifiers() const
+{
+ BYTE result = 0;
+ if(GetAsyncKeyState(VK_CONTROL) & 0x8000) result |= HOTKEYF_CONTROL;
+ if(GetAsyncKeyState(VK_SHIFT) & 0x8000) result |= HOTKEYF_SHIFT;
+ if(GetAsyncKeyState(VK_MENU) & 0x8000) result |= HOTKEYF_ALT;
+ if((GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000) result |= HOTKEYF_EXT;
+ return result;
+}
+bool CClipboardCarouselInput::InsidePreview(POINT point) const
+{
+ HWND window = WindowFromPoint(point);
+ return window == m_preview || IsChild(m_preview,window);
+}
+bool CClipboardCarouselInput::PostCommand(ClipboardCarouselCommand command)
+{
+ return PostMessage(m_owner,WM_CLIPBOARD_CAROUSEL_INPUT,(WPARAM)command,
+ command == ClipboardCarouselCommand::Activate ? 0 : (LPARAM)m_generation.load()) != FALSE;
+}
+LRESULT CClipboardCarouselInput::MouseEvent(WPARAM message, const MSLLHOOKSTRUCT& event)
+{
+ if(event.flags & LLMHF_INJECTED) return 0;
+ ClipboardCarouselTrigger trigger = ClipboardCarouselTrigger::None;
+ bool down = false, up = false;
+ if(message == WM_MBUTTONDOWN || message == WM_MBUTTONUP)
+ {
+ trigger = ClipboardCarouselTrigger::MiddleButton;
+ down = message == WM_MBUTTONDOWN; up = !down;
+ }
+ else if(message == WM_XBUTTONDOWN || message == WM_XBUTTONUP)
+ {
+ trigger = HIWORD(event.mouseData) == XBUTTON1 ? ClipboardCarouselTrigger::XButton1 : ClipboardCarouselTrigger::XButton2;
+ down = message == WM_XBUTTONDOWN; up = !down;
+ }
+ UINT mask = trigger == ClipboardCarouselTrigger::None ? 0 : 1U << (int)trigger;
+ if(up && mask && (m_swallowedButtons.load() & mask))
+ {
+ m_swallowedButtons.fetch_and(~mask);
+ return 1;
+ }
+ if(!m_enabled || m_suspended) return 0;
+ if(down && trigger == m_binding.trigger && Modifiers() == m_binding.modifiers)
+ {
+ m_swallowedButtons.fetch_or(mask);
+ if(!m_active) PostCommand(ClipboardCarouselCommand::Activate);
+ return 1;
+ }
+ if(!m_active) return 0;
+ if(message == WM_MOUSEWHEEL)
+ {
+ m_wheelRemainder += (SHORT)HIWORD(event.mouseData);
+ while(abs(m_wheelRemainder) >= WHEEL_DELTA)
+ {
+ bool upward = m_wheelRemainder > 0;
+ PostCommand(upward ? ClipboardCarouselCommand::Newer : ClipboardCarouselCommand::Older);
+ m_wheelRemainder += upward ? -WHEEL_DELTA : WHEEL_DELTA;
+ }
+ return 1;
+ }
+ if(message == WM_LBUTTONDOWN)
+ {
+ m_outsideDown = !InsidePreview(event.pt);
+ m_downRoot = GetAncestor(WindowFromPoint(event.pt),GA_ROOT);
+ m_downGeneration = m_generation;
+ }
+ else if(message == WM_LBUTTONUP && m_outsideDown)
+ {
+ m_outsideDown = false;
+ HWND clicked = WindowFromPoint(event.pt);
+ if(m_downGeneration == m_generation && !InsidePreview(event.pt) && GetAncestor(clicked,GA_ROOT) == m_downRoot)
+ {
+ m_clickedWindow = clicked;
+ m_clickX = event.pt.x; m_clickY = event.pt.y;
+ PostCommand(ClipboardCarouselCommand::ClickPaste);
+ }
+ }
+ else if((message == WM_RBUTTONDOWN || down) && !InsidePreview(event.pt))
+ PostCommand(ClipboardCarouselCommand::ClickCancel);
+ return 0;
+}
+LRESULT CClipboardCarouselInput::KeyboardEvent(WPARAM message, const KBDLLHOOKSTRUCT& event)
+{
+ if((event.flags & LLKHF_INJECTED) || event.vkCode >= 256) return 0;
+ bool down = message == WM_KEYDOWN || message == WM_SYSKEYDOWN;
+ if(message == WM_KEYUP || message == WM_SYSKEYUP)
+ {
+ if(m_swallowedKeys[event.vkCode])
+ {
+ m_swallowedKeys[event.vkCode] = false;
+ m_swallowedKeyCount--;
+ return 1;
+ }
+ return 0;
+ }
+ if(!down) return 0;
+ if(m_swallowedKeys[event.vkCode])
+ {
+ // Drain repeats as well as key-up after closing, without dispatching
+ // another command or forwarding a held Enter/Escape to the target.
+ if(m_active && !m_suspended && Modifiers() == 0 && (event.vkCode == VK_UP || event.vkCode == VK_DOWN))
+ PostCommand(event.vkCode == VK_UP ? ClipboardCarouselCommand::Newer : ClipboardCarouselCommand::Older);
+ return 1;
+ }
+ if(!m_active || m_suspended) return 0;
+ ClipboardCarouselCommand command;
+ if(event.vkCode == VK_ESCAPE) command = ClipboardCarouselCommand::Cancel;
+ else if(event.vkCode == VK_RETURN) command = ClipboardCarouselCommand::Confirm;
+ else if(Modifiers() == 0 && event.vkCode == VK_UP)
+ command = ClipboardCarouselCommand::Newer;
+ else if(Modifiers() == 0 && event.vkCode == VK_DOWN)
+ command = ClipboardCarouselCommand::Older;
+ else return 0;
+ m_swallowedKeys[event.vkCode] = true;
+ m_swallowedKeyCount++;
+ PostCommand(command);
+ return 1;
+}
diff --git a/src/ClipboardCarouselInput.h b/src/ClipboardCarouselInput.h
new file mode 100644
index 00000000..dab39e40
--- /dev/null
+++ b/src/ClipboardCarouselInput.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "ClipboardCarouselBinding.h"
+#include
+
+class CClipboardCarouselInput
+{
+public:
+ CClipboardCarouselInput();
+ ~CClipboardCarouselInput();
+ bool Initialize(HWND owner, HWND preview);
+ void Shutdown();
+ void Suspend();
+ bool Resume(CString& error);
+ bool HandleHotKey(WPARAM id);
+ bool BeginSession(CString& error);
+ void EndSession();
+ void Tick();
+ bool IsEnabled() const { return m_enabled; }
+ UINT_PTR Generation() const { return m_generation; }
+ HWND ClickedWindow() const { return m_clickedWindow; }
+ POINT ClickedPoint() const { POINT point = { m_clickX.load(), m_clickY.load() }; return point; }
+ static bool Validate(bool enabled, const ClipboardCarouselBinding& binding, CString& error);
+
+private:
+ bool UpdateHooks(bool keyboard, CString& error);
+ void StopHooks();
+ void UnregisterActivation();
+ bool PostCommand(ClipboardCarouselCommand command);
+ bool InsidePreview(POINT point) const;
+ BYTE Modifiers() const;
+ static DWORD WINAPI HookThreadProc(LPVOID parameter);
+ DWORD HookThread();
+ static LRESULT CALLBACK MouseHookProc(int code, WPARAM message, LPARAM data);
+ static LRESULT CALLBACK KeyboardHookProc(int code, WPARAM message, LPARAM data);
+ LRESULT MouseEvent(WPARAM message, const MSLLHOOKSTRUCT& event);
+ LRESULT KeyboardEvent(WPARAM message, const KBDLLHOOKSTRUCT& event);
+
+ HWND m_owner;
+ HWND m_preview;
+ ATOM m_atom;
+ bool m_registered;
+ ClipboardCarouselBinding m_binding;
+ std::atomic m_enabled;
+ std::atomic m_suspended;
+ std::atomic m_active;
+ std::atomic m_generation;
+ std::atomic m_clickedWindow;
+ std::atomic m_clickX, m_clickY;
+ std::atomic m_swallowedButtons;
+ std::atomic m_swallowedKeyCount;
+ bool m_swallowedKeys[256];
+ bool m_outsideDown;
+ HWND m_downRoot;
+ UINT_PTR m_downGeneration;
+ std::atomic m_wheelRemainder;
+
+ HANDLE m_thread;
+ DWORD m_threadId;
+ HANDLE m_ready;
+ std::atomic m_keyboardWanted;
+ std::atomic m_hooksReady;
+ HHOOK m_mouseHook;
+ HHOOK m_keyboardHook;
+ static CClipboardCarouselInput* s_owner;
+};
diff --git a/src/ClipboardCarouselWnd.cpp b/src/ClipboardCarouselWnd.cpp
new file mode 100644
index 00000000..21ac112c
--- /dev/null
+++ b/src/ClipboardCarouselWnd.cpp
@@ -0,0 +1,413 @@
+#include "stdafx.h"
+#include "CP_Main.h"
+#include "ClipboardCarouselWnd.h"
+#include "Misc.h"
+
+namespace
+{
+ // Dark palette shared with the other flyouts; logical sizes are DPI-scaled at use.
+ const COLORREF BACKGROUND = RGB(28,28,32);
+ const COLORREF SURFACE = RGB(37,37,43);
+ const COLORREF SURFACE_BORDER = RGB(50,50,58);
+ const COLORREF FOREGROUND = RGB(236,236,240);
+ const COLORREF MUTED = RGB(150,150,162);
+ const COLORREF BORDER = RGB(62,62,72);
+ const int OUTER_PADDING = 8;
+ const int INNER_PADDING = 12;
+ const int SURFACE_RADIUS = 8;
+ const int IMAGE_RADIUS = 6;
+
+ HMODULE LoadSystemLibrary(LPCTSTR name)
+ {
+ return LoadLibraryEx(name,NULL,LOAD_LIBRARY_SEARCH_SYSTEM32);
+ }
+
+ // Windows 11 rounds popup corners and draws the border; earlier versions report failure.
+ bool SetDwmAttribute(HWND window, DWORD attribute, DWORD value)
+ {
+ typedef HRESULT(WINAPI *DwmSetWindowAttributeFn)(HWND,DWORD,LPCVOID,DWORD);
+ static DwmSetWindowAttributeFn setAttribute = []() -> DwmSetWindowAttributeFn
+ {
+ HMODULE dwm = LoadSystemLibrary(_T("dwmapi.dll"));
+ return dwm ? (DwmSetWindowAttributeFn)GetProcAddress(dwm,"DwmSetWindowAttribute") : NULL;
+ }();
+ return setAttribute && SUCCEEDED(setAttribute(window,attribute,&value,sizeof(value)));
+ }
+
+ void SetControlTheme(HWND window, LPCWSTR theme)
+ {
+ typedef HRESULT(WINAPI *SetWindowThemeFn)(HWND,LPCWSTR,LPCWSTR);
+ static SetWindowThemeFn setTheme = []() -> SetWindowThemeFn
+ {
+ HMODULE uxtheme = LoadSystemLibrary(_T("uxtheme.dll"));
+ return uxtheme ? (SetWindowThemeFn)GetProcAddress(uxtheme,"SetWindowTheme") : NULL;
+ }();
+ if(setTheme) setTheme(window,theme,NULL);
+ }
+
+ Gdiplus::Color ToColor(COLORREF color)
+ {
+ return Gdiplus::Color(255,GetRValue(color),GetGValue(color),GetBValue(color));
+ }
+
+ void AddRoundRect(Gdiplus::GraphicsPath& path, const Gdiplus::RectF& rect, Gdiplus::REAL radius)
+ {
+ Gdiplus::REAL diameter = min(radius*2,min(rect.Width,rect.Height));
+ if(diameter < 1) { path.AddRectangle(rect); return; }
+ path.AddArc(rect.X,rect.Y,diameter,diameter,180,90);
+ path.AddArc(rect.GetRight()-diameter,rect.Y,diameter,diameter,270,90);
+ path.AddArc(rect.GetRight()-diameter,rect.GetBottom()-diameter,diameter,diameter,0,90);
+ path.AddArc(rect.X,rect.GetBottom()-diameter,diameter,diameter,90,90);
+ path.CloseFigure();
+ }
+}
+
+BEGIN_MESSAGE_MAP(CClipboardCarouselWnd, CWnd)
+ ON_WM_PAINT()
+ ON_WM_ERASEBKGND()
+ ON_WM_MOUSEACTIVATE()
+ ON_WM_SIZE()
+ ON_WM_CTLCOLOR()
+ ON_MESSAGE(WM_DPICHANGED, OnDpiChanged)
+END_MESSAGE_MAP()
+
+CClipboardCarouselWnd::CClipboardCarouselWnd()
+ : m_image(NULL), m_imagePreview(false), m_positioning(false), m_anchor{}, m_monitor(NULL),
+ m_background(BACKGROUND), m_foreground(FOREGROUND), m_border(BORDER), m_surface(SURFACE),
+ m_surfaceBorder(SURFACE_BORDER), m_muted(MUTED), m_roundedByDwm(false)
+{
+}
+
+CClipboardCarouselWnd::~CClipboardCarouselWnd() { DeleteImage(); }
+
+bool CClipboardCarouselWnd::Create(CWnd* owner)
+{
+ CString className = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW | CS_DROPSHADOW,
+ LoadCursor(NULL, IDC_ARROW), NULL, NULL);
+ if(!CreateEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW, className,
+ theApp.m_Language.GetString("ClipboardCarouselTitle", "Clipboard Carousel"),
+ WS_POPUP | WS_CLIPCHILDREN, CRect(0,0,1,1), owner, 0))
+ return false;
+ m_dpi.SetHwnd(m_hWnd);
+ if(!m_textEdit.Create(WS_CHILD | ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL,
+ CRect(0,0,1,1), this, 1))
+ return false;
+ RefreshColors();
+ m_textEdit.SetLimitText(4096);
+ RecreateFont();
+ return true;
+}
+
+void CClipboardCarouselWnd::RefreshColors()
+{
+ HIGHCONTRAST contrast = {};
+ contrast.cbSize = sizeof(contrast);
+ bool highContrast = SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(contrast), &contrast, 0) &&
+ (contrast.dwFlags & HCF_HIGHCONTRASTON);
+ m_background = highContrast ? GetSysColor(COLOR_WINDOW) : BACKGROUND;
+ m_foreground = highContrast ? GetSysColor(COLOR_WINDOWTEXT) : FOREGROUND;
+ m_border = highContrast ? GetSysColor(COLOR_WINDOWTEXT) : BORDER;
+ m_surface = highContrast ? m_background : SURFACE;
+ m_surfaceBorder = highContrast ? m_foreground : SURFACE_BORDER;
+ m_muted = highContrast ? m_foreground : MUTED;
+ m_textBrush.DeleteObject();
+ m_textBrush.CreateSolidBrush(m_surface);
+ ApplyWindowStyle(highContrast);
+ if(GetSafeHwnd()) Invalidate();
+}
+
+void CClipboardCarouselWnd::ApplyWindowStyle(bool highContrast)
+{
+ if(!GetSafeHwnd()) return;
+ const DWORD DWMWA_WINDOW_CORNER_PREFERENCE_ = 33, DWMWA_BORDER_COLOR_ = 34, DWMWCP_ROUND_ = 2;
+ m_roundedByDwm = SetDwmAttribute(m_hWnd,DWMWA_WINDOW_CORNER_PREFERENCE_,DWMWCP_ROUND_);
+ if(m_roundedByDwm) SetDwmAttribute(m_hWnd,DWMWA_BORDER_COLOR_,m_border);
+ // Dark scrollbar for the text preview; High Contrast keeps the system theme.
+ if(m_textEdit.GetSafeHwnd())
+ SetControlTheme(m_textEdit.GetSafeHwnd(),highContrast ? NULL : L"DarkMode_Explorer");
+}
+
+CRect CClipboardCarouselWnd::SurfaceRect()
+{
+ CRect surface;
+ GetClientRect(surface);
+ surface.DeflateRect(m_dpi.Scale(OUTER_PADDING),m_dpi.Scale(OUTER_PADDING));
+ return surface;
+}
+
+CRect CClipboardCarouselWnd::ContentRect()
+{
+ CRect content = SurfaceRect();
+ content.DeflateRect(m_dpi.Scale(INNER_PADDING),m_dpi.Scale(INNER_PADDING));
+ return content;
+}
+
+void CClipboardCarouselWnd::RecreateFont()
+{
+ LOGFONT font = {};
+ font.lfHeight = -m_dpi.Scale(15);
+ font.lfQuality = CLEARTYPE_QUALITY;
+ _tcscpy_s(font.lfFaceName, LF_FACESIZE, _T("Segoe UI"));
+ m_font.DeleteObject();
+ m_font.CreateFontIndirect(&font);
+ if(m_textEdit.GetSafeHwnd())
+ {
+ m_textEdit.SetFont(&m_font);
+ m_textEdit.SendMessage(EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN,
+ MAKELPARAM(m_dpi.Scale(3), m_dpi.Scale(3)));
+ }
+}
+
+void CClipboardCarouselWnd::DeleteImage()
+{
+ if(m_image) DeleteObject(m_image);
+ m_image = NULL;
+}
+
+void CClipboardCarouselWnd::SetPreview(const CString& text, bool imagePreview)
+{
+ DeleteImage();
+ // An image description can be a raw clipboard format name (such as CF_DIB).
+ // Keep its loading frame empty until the thumbnail is ready.
+ m_text = imagePreview ? CString() : text.Left(4096);
+ if(!m_text.IsEmpty() && m_text[m_text.GetLength()-1] >= 0xD800 && m_text[m_text.GetLength()-1] <= 0xDBFF)
+ m_text.Truncate(m_text.GetLength()-1);
+ m_imagePreview = imagePreview;
+ m_textEdit.SetWindowText(m_text);
+ m_textEdit.SetSel(0,0);
+ m_textEdit.ShowWindow(imagePreview ? SW_HIDE : SW_SHOWNA);
+ if(IsWindowVisible()) LayoutPreview();
+ Invalidate();
+}
+
+void CClipboardCarouselWnd::SetImage(HBITMAP image)
+{
+ DeleteImage();
+ m_image = image;
+ if(!image)
+ m_text = theApp.m_Language.GetString("ClipboardCarouselNoPreview","No preview available.");
+ if(IsWindowVisible()) Invalidate();
+}
+
+void CClipboardCarouselWnd::ShowPreview()
+{
+ if(!IsWindowVisible())
+ {
+ GetCursorPos(&m_anchor);
+ m_monitor = MonitorFromPoint(m_anchor, MONITOR_DEFAULTTONEAREST);
+ // Move a hidden, tiny window first so Windows establishes destination DPI.
+ // The final rectangle is then computed from the actual window DPI once.
+ m_positioning = true;
+ SetWindowPos(NULL, m_anchor.x, m_anchor.y, 1, 1, SWP_NOACTIVATE | SWP_NOZORDER);
+ typedef UINT(WINAPI *WindowDpi)(HWND);
+ static WindowDpi windowDpi = (WindowDpi)GetProcAddress(GetModuleHandle(_T("user32.dll")),"GetDpiForWindow");
+ if(windowDpi) m_dpi.Update(windowDpi(m_hWnd));
+ else m_dpi.SetHwnd(m_hWnd);
+ RecreateFont();
+ m_positioning = false;
+ }
+ LayoutPreview();
+ ShowWindow(SW_SHOWNOACTIVATE);
+}
+
+void CClipboardCarouselWnd::LayoutPreview()
+{
+ if(m_positioning || !GetSafeHwnd()) return;
+ MONITORINFO info = {};
+ info.cbSize = sizeof(info);
+ if(!GetMonitorInfo(m_monitor, &info)) return;
+ CRect work(info.rcWork);
+ int width = min(m_dpi.Scale(480), work.Width());
+ int height = min(m_dpi.Scale(300), work.Height());
+ int x = max(work.left, min(m_anchor.x + m_dpi.Scale(16), work.right - width));
+ int y = max(work.top, min(m_anchor.y + m_dpi.Scale(20), work.bottom - height));
+ m_positioning = true;
+ SetWindowPos(&wndTopMost, x, y, width, height, SWP_NOACTIVATE);
+ m_positioning = false;
+ PositionText();
+ Invalidate();
+}
+
+void CClipboardCarouselWnd::PositionText()
+{
+ if(!m_textEdit.GetSafeHwnd()) return;
+ CRect content = ContentRect();
+ m_textEdit.SetWindowPos(NULL, content.left, content.top, max(1,content.Width()),
+ max(1,content.Height()), SWP_NOZORDER | SWP_NOACTIVATE);
+ // Measure wrapped lines at the full available width before reserving a scrollbar.
+ m_textEdit.ShowScrollBar(SB_VERT,FALSE);
+ if(!m_imagePreview)
+ {
+ CRect formatting;
+ m_textEdit.GetRect(formatting);
+ CClientDC dc(&m_textEdit);
+ CFont* previous = m_font.GetSafeHandle() ? dc.SelectObject(&m_font) : NULL;
+ TEXTMETRIC metrics = {};
+ dc.GetTextMetrics(&metrics);
+ if(previous) dc.SelectObject(previous);
+ int visibleLines = max(1,formatting.Height() / max(1L,metrics.tmHeight));
+ m_textEdit.ShowScrollBar(SB_VERT,m_textEdit.GetLineCount() > visibleLines);
+ }
+}
+
+void CClipboardCarouselWnd::HidePreview()
+{
+ if(GetSafeHwnd()) ShowWindow(SW_HIDE);
+ DeleteImage();
+}
+
+bool CClipboardCarouselWnd::HasFocus() const
+{
+ HWND focus = ::GetFocus();
+ return focus == GetSafeHwnd() || (GetSafeHwnd() && ::IsChild(GetSafeHwnd(), focus));
+}
+
+CString CClipboardCarouselWnd::SelectedText() const
+{
+ if(m_imagePreview) return CString();
+ int start = 0, end = 0;
+ m_textEdit.GetSel(start, end);
+ CString displayed;
+ m_textEdit.GetWindowText(displayed);
+ return end > start ? displayed.Mid(start, end - start) : CString();
+}
+
+BOOL CClipboardCarouselWnd::PreTranslateMessage(MSG* message)
+{
+ if(message->message == WM_KEYDOWN && HasFocus() && (::GetKeyState(VK_CONTROL) & 0x8000))
+ {
+ if(message->wParam == 'A')
+ {
+ m_textEdit.SetSel(0,-1);
+ return TRUE;
+ }
+ if(message->wParam == 'C')
+ {
+ CString text = SelectedText();
+ if(!text.IsEmpty())
+ {
+ HGLOBAL data = NewGlobalP((LPVOID)(LPCTSTR)text, (text.GetLength()+1)*sizeof(TCHAR));
+ if(data)
+ {
+ COleDataSource* source = new COleDataSource;
+ source->CacheGlobalData(CF_UNICODETEXT, data);
+ source->SetClipboard();
+ }
+ }
+ return TRUE;
+ }
+ }
+ return CWnd::PreTranslateMessage(message);
+}
+
+void CClipboardCarouselWnd::OnPaint()
+{
+ CPaintDC paint(this);
+ CRect client;
+ GetClientRect(client);
+ if(client.Width() <= 0 || client.Height() <= 0) return;
+
+ // Compose off-screen so the rounded surface and image never flicker while cycling.
+ CDC dc;
+ dc.CreateCompatibleDC(&paint);
+ CBitmap buffer;
+ buffer.CreateCompatibleBitmap(&paint, client.Width(), client.Height());
+ CBitmap* previousBuffer = dc.SelectObject(&buffer);
+ dc.FillSolidRect(client, m_background);
+ if(!m_roundedByDwm) dc.Draw3dRect(client, m_border, m_border);
+
+ CRect surface = SurfaceRect();
+ Gdiplus::REAL line = (Gdiplus::REAL)max(1,m_dpi.Scale(1));
+ {
+ Gdiplus::Graphics graphics(dc.GetSafeHdc());
+ graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
+ graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
+ Gdiplus::GraphicsPath path;
+ AddRoundRect(path, Gdiplus::RectF(surface.left+line/2, surface.top+line/2,
+ surface.Width()-line, surface.Height()-line), (Gdiplus::REAL)m_dpi.Scale(SURFACE_RADIUS));
+ Gdiplus::SolidBrush fill(ToColor(m_surface));
+ graphics.FillPath(&fill, &path);
+ Gdiplus::Pen pen(ToColor(m_surfaceBorder), line);
+ graphics.DrawPath(&pen, &path);
+ }
+
+ CRect content = ContentRect();
+ if(m_imagePreview && content.Width() > 0 && content.Height() > 0)
+ {
+ if(m_image)
+ {
+ BITMAP bitmap = {};
+ GetObject(m_image, sizeof(bitmap), &bitmap);
+ CDC imageDC;
+ imageDC.CreateCompatibleDC(&dc);
+ HGDIOBJ previous = imageDC.SelectObject(m_image);
+ double scale = min((double)content.Width()/max(1L,bitmap.bmWidth),
+ (double)content.Height()/max(1L,bitmap.bmHeight));
+ int width = max(1,(int)(bitmap.bmWidth*scale));
+ int height = max(1,(int)(bitmap.bmHeight*scale));
+ int x = content.CenterPoint().x-width/2, y = content.CenterPoint().y-height/2;
+ dc.SetStretchBltMode(HALFTONE);
+ SetBrushOrgEx(dc.GetSafeHdc(), 0, 0, NULL);
+ dc.StretchBlt(x, y, width, height, &imageDC, 0,0, bitmap.bmWidth,bitmap.bmHeight, SRCCOPY);
+ imageDC.SelectObject(previous);
+
+ // Round the image corners by painting the surface back over them, anti-aliased.
+ Gdiplus::Graphics graphics(dc.GetSafeHdc());
+ graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
+ graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
+ Gdiplus::GraphicsPath corners(Gdiplus::FillModeAlternate);
+ corners.AddRectangle(Gdiplus::RectF((Gdiplus::REAL)x-1, (Gdiplus::REAL)y-1,
+ (Gdiplus::REAL)width+2, (Gdiplus::REAL)height+2));
+ AddRoundRect(corners, Gdiplus::RectF((Gdiplus::REAL)x, (Gdiplus::REAL)y,
+ (Gdiplus::REAL)width, (Gdiplus::REAL)height), (Gdiplus::REAL)m_dpi.Scale(IMAGE_RADIUS));
+ Gdiplus::SolidBrush fill(ToColor(m_surface));
+ graphics.FillPath(&fill, &corners);
+ }
+ else if(!m_text.IsEmpty())
+ {
+ CFont* previous = dc.SelectObject(&m_font);
+ dc.SetTextColor(m_muted);
+ dc.SetBkMode(TRANSPARENT);
+ CRect measured = content;
+ dc.DrawText(m_text, measured, DT_WORDBREAK | DT_NOPREFIX | DT_CENTER | DT_CALCRECT);
+ CRect text = content;
+ text.top = max(content.top, content.CenterPoint().y - measured.Height()/2);
+ dc.DrawText(m_text, text, DT_WORDBREAK | DT_NOPREFIX | DT_CENTER);
+ dc.SelectObject(previous);
+ }
+ }
+
+ paint.BitBlt(0, 0, client.Width(), client.Height(), &dc, 0, 0, SRCCOPY);
+ dc.SelectObject(previousBuffer);
+}
+
+BOOL CClipboardCarouselWnd::OnEraseBkgnd(CDC*) { return TRUE; }
+int CClipboardCarouselWnd::OnMouseActivate(CWnd*, UINT, UINT) { return MA_ACTIVATE; }
+void CClipboardCarouselWnd::OnSize(UINT type, int cx, int cy)
+{
+ CWnd::OnSize(type,cx,cy);
+ PositionText();
+}
+HBRUSH CClipboardCarouselWnd::OnCtlColor(CDC* dc, CWnd* wnd, UINT type)
+{
+ if(wnd && wnd->GetSafeHwnd() == m_textEdit.GetSafeHwnd())
+ {
+ LOGBRUSH brush = {};
+ m_textBrush.GetLogBrush(&brush);
+ dc->SetBkColor(brush.lbColor);
+ dc->SetTextColor(m_foreground);
+ return (HBRUSH)m_textBrush.GetSafeHandle();
+ }
+ return CWnd::OnCtlColor(dc,wnd,type);
+}
+LRESULT CClipboardCarouselWnd::OnDpiChanged(WPARAM dpi, LPARAM)
+{
+ m_dpi.Update(HIWORD(dpi));
+ RecreateFont();
+ if(!m_positioning)
+ {
+ m_monitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
+ LayoutPreview();
+ }
+ return 0;
+}
diff --git a/src/ClipboardCarouselWnd.h b/src/ClipboardCarouselWnd.h
new file mode 100644
index 00000000..58f033b5
--- /dev/null
+++ b/src/ClipboardCarouselWnd.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "DPI.h"
+
+class CClipboardCarouselWnd : public CWnd
+{
+public:
+ CClipboardCarouselWnd();
+ virtual ~CClipboardCarouselWnd();
+ bool Create(CWnd* owner);
+ void SetPreview(const CString& text, bool imagePreview = false);
+ void SetImage(HBITMAP image);
+ void ShowPreview();
+ void HidePreview();
+ void RefreshColors();
+ CString SelectedText() const;
+ bool HasFocus() const;
+ bool IsImagePreview() const { return m_imagePreview; }
+ // Image thumbnails are composed onto the content surface they are drawn on.
+ COLORREF BackgroundColor() const { return m_surface; }
+ virtual BOOL PreTranslateMessage(MSG* message);
+
+protected:
+ afx_msg void OnPaint();
+ afx_msg BOOL OnEraseBkgnd(CDC*);
+ afx_msg int OnMouseActivate(CWnd*, UINT, UINT);
+ afx_msg void OnSize(UINT type, int cx, int cy);
+ afx_msg HBRUSH OnCtlColor(CDC* dc, CWnd* wnd, UINT ctlColor);
+ afx_msg LRESULT OnDpiChanged(WPARAM wParam, LPARAM lParam);
+ DECLARE_MESSAGE_MAP()
+
+private:
+ void LayoutPreview();
+ void PositionText();
+ void RecreateFont();
+ void DeleteImage();
+ void ApplyWindowStyle(bool highContrast);
+ CRect SurfaceRect();
+ CRect ContentRect();
+
+ CDPI m_dpi;
+ CEdit m_textEdit;
+ CFont m_font;
+ CBrush m_textBrush;
+ CString m_text;
+ HBITMAP m_image;
+ bool m_imagePreview;
+ bool m_positioning;
+ POINT m_anchor;
+ HMONITOR m_monitor;
+ COLORREF m_background;
+ COLORREF m_foreground;
+ COLORREF m_border;
+ COLORREF m_surface;
+ COLORREF m_surfaceBorder;
+ COLORREF m_muted;
+ bool m_roundedByDwm;
+};
diff --git a/src/ExternalWindowTracker.cpp b/src/ExternalWindowTracker.cpp
index 88641d40..fc71a96f 100644
--- a/src/ExternalWindowTracker.cpp
+++ b/src/ExternalWindowTracker.cpp
@@ -116,6 +116,18 @@ bool ExternalWindowTracker::TrackActiveWnd(bool force)
return true;
}
+bool ExternalWindowTracker::RestoreTrackedWnd(HWND activeWnd, HWND focusWnd)
+{
+ if(!IsWindow(activeWnd))
+ return false;
+
+ m_activeWnd = activeWnd;
+ m_focusWnd = IsWindow(focusWnd) && GetAncestor(focusWnd, GA_ROOT) == activeWnd ? focusWnd : activeWnd;
+ m_dittoHasFocus = false;
+ m_desktopHasFocus = false;
+ return true;
+}
+
bool ExternalWindowTracker::WaitForActiveWnd(HWND activeWnd, int timeout)
{
DWORD start = GetTickCount();
diff --git a/src/ExternalWindowTracker.h b/src/ExternalWindowTracker.h
index d8fbe9c4..2c748f5b 100644
--- a/src/ExternalWindowTracker.h
+++ b/src/ExternalWindowTracker.h
@@ -16,6 +16,7 @@ class ExternalWindowTracker
CString ActiveWndName();
CString WndName(HWND hWnd);
bool TrackActiveWnd(bool force);
+ bool RestoreTrackedWnd(HWND activeWnd, HWND focusWnd);
bool ActivateTarget();
bool ReleaseFocus();
CPoint FocusCaret();
diff --git a/src/MainFrm.cpp b/src/MainFrm.cpp
index b3c4de1e..0ef88f89 100644
--- a/src/MainFrm.cpp
+++ b/src/MainFrm.cpp
@@ -81,6 +81,8 @@ BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_FIRST_DELETEALLNONUSEDCLIPS, &CMainFrame::OnFirstDeleteallnonusedclips)
ON_MESSAGE(WM_PASTE_CLIP, OnPasteClip)
ON_MESSAGE(WM_EDIT_CLIP, OnEditClip)
+ ON_MESSAGE(WM_CLIPBOARD_CAROUSEL_INPUT, OnClipboardCarouselInput)
+ ON_MESSAGE(WM_CLIPBOARD_CAROUSEL_PREVIEW_READY, OnClipboardCarouselPreviewReady)
ON_WM_SETFOCUS()
END_MESSAGE_MAP()
@@ -181,6 +183,12 @@ int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
m_ulCopyGap = CGetSetOptions::GetCopyGap();
theApp.AfterMainCreate();
+ if(!m_clipboardCarousel.Initialize(this))
+ {
+ Log(_T("Unable to initialize Clipboard Carousel"));
+ }
+ if(m_clipboardCarousel.IsEnabled())
+ SetTimer(CLIPBOARD_CAROUSEL_TIMER, 30, NULL);
m_thread.Start(this);
@@ -275,6 +283,11 @@ void CMainFrame::OnFirstExit()
LRESULT CMainFrame::OnHotKey(WPARAM wParam, LPARAM lParam)
{
+ if(m_clipboardCarousel.HandleHotKey(wParam))
+ return TRUE;
+ if(m_clipboardCarousel.IsActive())
+ m_clipboardCarousel.Cancel();
+
if(theApp.m_pDittoHotKey && wParam == theApp.m_pDittoHotKey->m_Atom ||
theApp.m_pDittoHotKey2 && wParam == theApp.m_pDittoHotKey2->m_Atom ||
theApp.m_pDittoHotKey3 && wParam == theApp.m_pDittoHotKey3->m_Atom)
@@ -492,6 +505,7 @@ LRESULT CMainFrame::OnHotKey(WPARAM wParam, LPARAM lParam)
void CMainFrame::ShowQPasteWithActiveWindowCheck()
{
+ m_clipboardCarousel.Cancel();
//Before we show our window find the current focused window for paste into
theApp.m_activeWnd.TrackActiveWnd(true);
@@ -789,7 +803,7 @@ void CMainFrame::OnTimer(UINT_PTR nIDEvent)
case ACTIVE_WINDOW_TIMER:
{
- if(theApp.m_bShowingQuickPaste)
+ if(!m_clipboardCarousel.IsActive() && theApp.m_bShowingQuickPaste)
{
theApp.m_activeWnd.TrackActiveWnd(false);
}
@@ -817,6 +831,7 @@ void CMainFrame::OnTimer(UINT_PTR nIDEvent)
{
if (theApp.EnterGroupID(m_doubleClickGroupId, FALSE, TRUE))
{
+ m_clipboardCarousel.Cancel();
theApp.m_activeWnd.TrackActiveWnd(true);
StartKeyModifierTimer();
m_quickPaste.ShowQPasteWnd(this, false, true, FALSE);
@@ -842,6 +857,7 @@ void CMainFrame::OnTimer(UINT_PTR nIDEvent)
case DELAYED_SHOW_DITTO_TIMER:
{
KillTimer(DELAYED_SHOW_DITTO_TIMER);
+ m_clipboardCarousel.Cancel();
m_quickPaste.ShowQPasteWnd(this, false, false, FALSE);
}
break;
@@ -853,6 +869,7 @@ void CMainFrame::OnTimer(UINT_PTR nIDEvent)
if (theme == _T(""))
{
CGetSetOptions::m_Theme.Load(theme);
+ m_clipboardCarousel.RefreshColors();
auto visible = m_quickPaste.IsWindowVisibleEx();
m_quickPaste.CloseQPasteWnd();
@@ -870,6 +887,10 @@ void CMainFrame::OnTimer(UINT_PTR nIDEvent)
theApp.CloseNoDbWindow();
break;
+ case CLIPBOARD_CAROUSEL_TIMER:
+ m_clipboardCarousel.Tick();
+ break;
+
}
CFrameWnd::OnTimer(nIDEvent);
@@ -900,6 +921,7 @@ LRESULT CMainFrame::OnShowTrayIcon(WPARAM wParam, LPARAM lParam)
void CMainFrame::OnFirstShowquickpaste()
{
+ m_clipboardCarousel.Cancel();
m_quickPaste.ShowQPasteWnd(this, false, false, FALSE);
}
@@ -966,6 +988,8 @@ void CMainFrame::OnClose()
}
CloseAllOpenDialogs();
+ KillTimer(CLIPBOARD_CAROUSEL_TIMER);
+ m_clipboardCarousel.Shutdown();
Log(_T("OnClose - before stop MainFrm thread"));
m_thread.Stop();
@@ -1231,6 +1255,8 @@ LRESULT CMainFrame::OnOpenCloseWindow(WPARAM wParam, LPARAM lParam)
void CMainFrame::OnDestroy()
{
+ m_clipboardCarousel.Shutdown();
+
if (m_pEditFrameWnd)
{
m_pEditFrameWnd->DestroyWindow();
@@ -1254,14 +1280,23 @@ void CMainFrame::OnFirstOption()
}
else
{
+ m_clipboardCarousel.SuspendInput();
m_pOptions = new COptionsSheet(_T(""));
if(m_pOptions != NULL)
{
((COptionsSheet*)m_pOptions)->SetNotifyWnd(m_hWnd);
- m_pOptions->Create();
- m_pOptions->ShowWindow(SW_SHOW);
+ if(m_pOptions->Create())
+ m_pOptions->ShowWindow(SW_SHOW);
+ else
+ {
+ delete m_pOptions;
+ m_pOptions = NULL;
+ m_clipboardCarousel.ResumeInput();
+ }
}
+ else
+ m_clipboardCarousel.ResumeInput();
}
}
@@ -1302,10 +1337,16 @@ LRESULT CMainFrame::OnOptionsClosed(WPARAM wParam, LPARAM lParam)
delete m_pOptions;
m_pOptions = NULL;
+ m_clipboardCarousel.ResumeInput();
+ if(m_clipboardCarousel.IsEnabled())
+ SetTimer(CLIPBOARD_CAROUSEL_TIMER, 30, NULL);
+ else
+ KillTimer(CLIPBOARD_CAROUSEL_TIMER);
if (themeChanged)
{
CGetSetOptions::m_Theme.Load(CGetSetOptions::GetTheme());
+ m_clipboardCarousel.RefreshColors();
m_quickPaste.CloseQPasteWnd();
}
@@ -1444,6 +1485,7 @@ LRESULT CMainFrame::OnReOpenDatabase(WPARAM wParam, LPARAM lParam)
{
Sleep(CGetSetOptions::GetWindowsResumeDelayReOpenDbMS());
m_quickPaste.CloseQPasteWnd();
+ m_clipboardCarousel.BeforeDatabaseClose();
theApp.m_db.close();
OpenDatabase(CGetSetOptions::GetDBPath());
}
@@ -1454,6 +1496,18 @@ LRESULT CMainFrame::OnReOpenDatabase(WPARAM wParam, LPARAM lParam)
return TRUE;
}
+LRESULT CMainFrame::OnClipboardCarouselInput(WPARAM wParam, LPARAM lParam)
+{
+ m_clipboardCarousel.HandleCommand((ClipboardCarouselCommand)wParam, (UINT_PTR)lParam);
+ return 0;
+}
+
+LRESULT CMainFrame::OnClipboardCarouselPreviewReady(WPARAM wParam, LPARAM)
+{
+ m_clipboardCarousel.HandlePreviewResult((ClipboardCarouselPreviewResult*)wParam);
+ return 0;
+}
+
LRESULT CMainFrame::OnShowMsgWindow(WPARAM wParam, LPARAM lParam)
{
CString *pMsg = (CString*)wParam;
diff --git a/src/MainFrm.h b/src/MainFrm.h
index 41d4fe4f..78ed095a 100644
--- a/src/MainFrm.h
+++ b/src/MainFrm.h
@@ -9,6 +9,7 @@
#include "DittoPopupWindow.h"
#include "NTray.h"
#include "EditFrameWnd.h"
+#include "ClipboardCarousel.h"
#define CLOSE_WINDOW_TIMER 1
#define HIDE_ICON_TIMER 2
@@ -25,6 +26,7 @@
#define DELAYED_SHOW_DITTO_TIMER 16
#define SET_WINDOWS_THEME_TIMER 17
#define CLOSE_NO_DB_WINDOW_TIMER 18
+#define CLIPBOARD_CAROUSEL_TIMER 19
class CMainFrame: public CFrameWnd
{
@@ -75,6 +77,7 @@ class CMainFrame: public CFrameWnd
int m_doubleClickGroupId;
DWORD m_doubleClickGroupStartTime;
CPowerManager m_PowerManager;
+ CClipboardCarousel m_clipboardCarousel;
int m_startupScreenWidth;
int m_startupScreenHeight;
CRichEditCtrlEx m_richEditTextConverter;
@@ -153,5 +156,7 @@ DECLARE_MESSAGE_MAP()public:
afx_msg void OnFirstDeleteallnonusedclips();
afx_msg LRESULT OnPasteClip(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnEditClip(WPARAM wParam, LPARAM lParam);
+ afx_msg LRESULT OnClipboardCarouselInput(WPARAM wParam, LPARAM lParam);
+ afx_msg LRESULT OnClipboardCarouselPreviewReady(WPARAM wParam, LPARAM lParam);
afx_msg void OnSetFocus(CWnd* pOldWnd);
};
diff --git a/src/Options.cpp b/src/Options.cpp
index 3bb1a932..25059cb0 100644
--- a/src/Options.cpp
+++ b/src/Options.cpp
@@ -3285,4 +3285,29 @@ void CGetSetOptions::SetEnforceClipboardIgnoreFormats(BOOL val)
BOOL CGetSetOptions::GetEnforceClipboardIgnoreFormats()
{
return GetProfileLong("EnforceClipboardIgnoreFormats", TRUE);
-}
\ No newline at end of file
+}
+
+void CGetSetOptions::SetCarouselEnabled(BOOL value)
+{
+ SetProfileLong("ClipboardCarouselSlimEnabled",value);
+}
+BOOL CGetSetOptions::GetCarouselEnabled()
+{
+ return GetProfileLong("ClipboardCarouselSlimEnabled",FALSE);
+}
+void CGetSetOptions::SetCarouselActivation(DWORD value)
+{
+ SetProfileLong("ClipboardCarouselSlimActivation",value);
+}
+DWORD CGetSetOptions::GetCarouselActivation()
+{
+ return (DWORD)GetProfileLong("ClipboardCarouselSlimActivation",0);
+}
+void CGetSetOptions::SetCarouselPasteAtClick(BOOL value)
+{
+ SetProfileLong("ClipboardCarouselSlimPasteAtClick",value);
+}
+BOOL CGetSetOptions::GetCarouselPasteAtClick()
+{
+ return GetProfileLong("ClipboardCarouselSlimPasteAtClick",TRUE);
+}
diff --git a/src/Options.h b/src/Options.h
index 9f1196f7..e5e4ba37 100644
--- a/src/Options.h
+++ b/src/Options.h
@@ -743,7 +743,14 @@ class CGetSetOptions
static BOOL m_enforceClipboardIgnoreFormats;
static void SetEnforceClipboardIgnoreFormats(BOOL val);
static BOOL GetEnforceClipboardIgnoreFormats();
+
+ static void SetCarouselEnabled(BOOL value);
+ static BOOL GetCarouselEnabled();
+ static void SetCarouselActivation(DWORD value);
+ static DWORD GetCarouselActivation();
+ static void SetCarouselPasteAtClick(BOOL value);
+ static BOOL GetCarouselPasteAtClick();
};
// global for easy access and for initialization of fast access variables
-extern CGetSetOptions g_Opt;
\ No newline at end of file
+extern CGetSetOptions g_Opt;
diff --git a/src/OptionsClipboardCarousel.cpp b/src/OptionsClipboardCarousel.cpp
new file mode 100644
index 00000000..87ab0e32
--- /dev/null
+++ b/src/OptionsClipboardCarousel.cpp
@@ -0,0 +1,126 @@
+#include "stdafx.h"
+#include "CP_Main.h"
+#include "OptionsClipboardCarousel.h"
+#include "ClipboardCarouselInput.h"
+#include "Options.h"
+
+IMPLEMENT_DYNAMIC(COptionsClipboardCarousel,CPropertyPage)
+BEGIN_MESSAGE_MAP(COptionsClipboardCarousel,CPropertyPage)
+ ON_BN_CLICKED(IDC_CC_KEYBOARD,OnKeyboard)
+ ON_BN_CLICKED(IDC_CC_MOUSE,OnMouse)
+ ON_BN_CLICKED(IDC_CC_CLEAR,OnClear)
+END_MESSAGE_MAP()
+
+COptionsClipboardCarousel::COptionsClipboardCarousel()
+ : CPropertyPage(IDD), m_initialEnabled(false)
+{
+ m_title = theApp.m_Language.GetString("ClipboardCarouselTitle","Clipboard Carousel");
+ m_psp.pszTitle = m_title;
+ m_psp.dwFlags |= PSP_USETITLE;
+}
+void COptionsClipboardCarousel::DoDataExchange(CDataExchange* exchange)
+{
+ CPropertyPage::DoDataExchange(exchange);
+ DDX_Control(exchange,IDC_CC_HOTKEY,m_hotKey);
+ DDX_Control(exchange,IDC_CC_MOUSE_TRIGGER,m_mouseButton);
+}
+BOOL COptionsClipboardCarousel::OnInitDialog()
+{
+ CPropertyPage::OnInitDialog();
+ struct Label { int id; const char* key; const char* fallback; };
+ const Label labels[] = {
+ { IDC_CC_ENABLE,"ClipboardCarouselEnable","Enable Clipboard Carousel" },
+ { IDC_CC_EDITOR_GROUP,"ClipboardCarouselActivation","Activate carousel" },
+ { IDC_CC_KEYBOARD,"ClipboardCarouselKeyboardShortcut","Keyboard shortcut" },
+ { IDC_CC_MOUSE,"ClipboardCarouselMouseWithModifiers","Mouse button + modifiers" },
+ { IDC_CC_MODIFIERS_LABEL,"ClipboardCarouselMouseModifiers","Mouse modifiers (optional):" },
+ { IDC_CC_CLEAR,"ClipboardCarouselClear","Clear" },
+ { IDC_CC_SHIFT,"ClipboardCarouselModifierShift","Shift" },
+ { IDC_CC_CONTROL,"ClipboardCarouselModifierCtrl","Ctrl" },
+ { IDC_CC_ALT,"ClipboardCarouselModifierAlt","Alt" },
+ { IDC_CC_PASTE_CLICK,"ClipboardCarouselPasteAtClick","Paste when clicking outside the carousel" },
+ { IDC_CC_HELP,"ClipboardCarouselSlimNavigationHelp",
+ "Down: older; Up: newer (wheel or arrows). Enter: paste. Escape: cancel.\r\nClick inside to select text; Ctrl+C copies the selection without closing.\r\nAn outside left click pastes the selection or the complete clip.\r\nNavigation wraps at both ends. Disable outside-click paste to cancel instead.\r\nOpened from the desktop: click the desktop to save an image as PNG." }
+ };
+ for(const auto& label : labels)
+ SetDlgItemText(label.id,theApp.m_Language.GetString(label.key,label.fallback));
+ m_mouseButton.AddString(theApp.m_Language.GetString("ClipboardCarouselMiddleButton","Middle Button"));
+ m_mouseButton.AddString(theApp.m_Language.GetString("ClipboardCarouselXButton1","XButton1"));
+ m_mouseButton.AddString(theApp.m_Language.GetString("ClipboardCarouselXButton2","XButton2"));
+ m_initialBinding = ClipboardCarouselBinding::Decode(CGetSetOptions::GetCarouselActivation());
+ m_initialEnabled = CGetSetOptions::GetCarouselEnabled() != FALSE;
+ CheckDlgButton(IDC_CC_ENABLE,m_initialEnabled ? BST_CHECKED : BST_UNCHECKED);
+ CheckDlgButton(IDC_CC_PASTE_CLICK,CGetSetOptions::GetCarouselPasteAtClick() ? BST_CHECKED : BST_UNCHECKED);
+ CheckRadioButton(IDC_CC_KEYBOARD,IDC_CC_MOUSE,m_initialBinding.IsMouse() ? IDC_CC_MOUSE : IDC_CC_KEYBOARD);
+ m_hotKey.SetHotKey(m_initialBinding.virtualKey,m_initialBinding.modifiers);
+ m_mouseButton.SetCurSel(m_initialBinding.IsMouse() ? (int)m_initialBinding.trigger - (int)ClipboardCarouselTrigger::MiddleButton : 0);
+ CheckDlgButton(IDC_CC_SHIFT,(m_initialBinding.modifiers & HOTKEYF_SHIFT) ? BST_CHECKED : BST_UNCHECKED);
+ CheckDlgButton(IDC_CC_CONTROL,(m_initialBinding.modifiers & HOTKEYF_CONTROL) ? BST_CHECKED : BST_UNCHECKED);
+ CheckDlgButton(IDC_CC_ALT,(m_initialBinding.modifiers & HOTKEYF_ALT) ? BST_CHECKED : BST_UNCHECKED);
+ SelectInput(!m_initialBinding.IsMouse());
+ return TRUE;
+}
+void COptionsClipboardCarousel::OnKeyboard() { SelectInput(true); }
+void COptionsClipboardCarousel::OnMouse() { SelectInput(false); }
+void COptionsClipboardCarousel::SelectInput(bool keyboard)
+{
+ CheckRadioButton(IDC_CC_KEYBOARD,IDC_CC_MOUSE,keyboard ? IDC_CC_KEYBOARD : IDC_CC_MOUSE);
+ GetDlgItem(IDC_CC_HOTKEY)->ShowWindow(keyboard ? SW_SHOW : SW_HIDE);
+ GetDlgItem(IDC_CC_MOUSE_TRIGGER)->ShowWindow(keyboard ? SW_HIDE : SW_SHOW);
+ const int modifierControls[] = { IDC_CC_MODIFIERS_LABEL,IDC_CC_SHIFT,IDC_CC_CONTROL,IDC_CC_ALT };
+ for(int id : modifierControls) GetDlgItem(id)->EnableWindow(!keyboard);
+}
+void COptionsClipboardCarousel::OnClear()
+{
+ m_hotKey.SetHotKey(0,0);
+ CheckDlgButton(IDC_CC_SHIFT,BST_UNCHECKED);
+ CheckDlgButton(IDC_CC_CONTROL,BST_UNCHECKED);
+ CheckDlgButton(IDC_CC_ALT,BST_UNCHECKED);
+ SelectInput(true);
+}
+ClipboardCarouselBinding COptionsClipboardCarousel::ReadBinding()
+{
+ ClipboardCarouselBinding binding;
+ if(IsDlgButtonChecked(IDC_CC_KEYBOARD) == BST_CHECKED)
+ {
+ WORD key = 0, modifiers = 0;
+ m_hotKey.GetHotKey(key,modifiers);
+ binding.virtualKey = (BYTE)key;
+ binding.modifiers = (BYTE)modifiers;
+ binding.modifiers &= HOTKEYF_SHIFT | HOTKEYF_CONTROL | HOTKEYF_ALT;
+ if(binding.virtualKey) binding.trigger = ClipboardCarouselTrigger::Keyboard;
+ }
+ else
+ {
+ binding.trigger = (ClipboardCarouselTrigger)((int)ClipboardCarouselTrigger::MiddleButton + max(0,m_mouseButton.GetCurSel()));
+ if(IsDlgButtonChecked(IDC_CC_SHIFT) == BST_CHECKED) binding.modifiers |= HOTKEYF_SHIFT;
+ if(IsDlgButtonChecked(IDC_CC_CONTROL) == BST_CHECKED) binding.modifiers |= HOTKEYF_CONTROL;
+ if(IsDlgButtonChecked(IDC_CC_ALT) == BST_CHECKED) binding.modifiers |= HOTKEYF_ALT;
+ }
+ return binding;
+}
+BOOL COptionsClipboardCarousel::OnApply()
+{
+ bool enabled = IsDlgButtonChecked(IDC_CC_ENABLE) == BST_CHECKED;
+ ClipboardCarouselBinding binding = ReadBinding();
+ CString error;
+ if(!CClipboardCarouselInput::Validate(enabled,binding,error))
+ {
+ MessageBox(error,m_title,MB_OK | MB_ICONWARNING);
+ return FALSE;
+ }
+ if(enabled && binding.IsMouse() && binding.modifiers == 0 &&
+ (!m_initialEnabled || binding.Encode() != m_initialBinding.Encode()))
+ {
+ if(MessageBox(theApp.m_Language.GetString("ClipboardCarouselBareMouseWarning",
+ "This mouse button will replace its normal application action while Clipboard Carousel is enabled. Continue?"),
+ m_title,MB_YESNO | MB_ICONWARNING) != IDYES)
+ return FALSE;
+ }
+ CGetSetOptions::SetCarouselEnabled(enabled);
+ CGetSetOptions::SetCarouselActivation(binding.Encode());
+ CGetSetOptions::SetCarouselPasteAtClick(IsDlgButtonChecked(IDC_CC_PASTE_CLICK) == BST_CHECKED);
+ m_initialEnabled = enabled;
+ m_initialBinding = binding;
+ return CPropertyPage::OnApply();
+}
diff --git a/src/OptionsClipboardCarousel.h b/src/OptionsClipboardCarousel.h
new file mode 100644
index 00000000..ceafea45
--- /dev/null
+++ b/src/OptionsClipboardCarousel.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "ClipboardCarouselBinding.h"
+
+class COptionsClipboardCarousel : public CPropertyPage
+{
+ DECLARE_DYNAMIC(COptionsClipboardCarousel)
+public:
+ COptionsClipboardCarousel();
+ enum { IDD = IDD_OPTIONS_CLIPBOARD_CAROUSEL };
+protected:
+ virtual void DoDataExchange(CDataExchange* exchange);
+ virtual BOOL OnInitDialog();
+ virtual BOOL OnApply();
+ afx_msg void OnKeyboard();
+ afx_msg void OnMouse();
+ afx_msg void OnClear();
+ DECLARE_MESSAGE_MAP()
+private:
+ ClipboardCarouselBinding ReadBinding();
+ void SelectInput(bool keyboard);
+ CString m_title;
+ CHotKeyCtrl m_hotKey;
+ CComboBox m_mouseButton;
+ ClipboardCarouselBinding m_initialBinding;
+ bool m_initialEnabled;
+};
diff --git a/src/OptionsSheet.cpp b/src/OptionsSheet.cpp
index 1c1a97e6..7a0cf4af 100644
--- a/src/OptionsSheet.cpp
+++ b/src/OptionsSheet.cpp
@@ -14,6 +14,7 @@
#include "OptionsCopyBuffers.h"
#include "Misc.h"
#include "QuickPasteKeyboard.h"
+#include "OptionsClipboardCarousel.h"
#ifdef _DEBUG
#define new DEBUG_NEW
@@ -41,6 +42,7 @@ COptionsSheet::COptionsSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectP
m_pFriends = NULL;
m_pCopyBuffers = NULL;
m_pQuickPasteShortCuts = NULL;
+ m_pClipboardCarousel = NULL;
m_pUtilites = NULL;
m_hWndParent = NULL;
@@ -51,6 +53,7 @@ COptionsSheet::COptionsSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectP
m_pKeyBoardOptions = new COptionsKeyBoard;
//m_pQuickPasteOptions = new COptionsQuickPaste;
m_pQuickPasteShortCuts = new CQuickPasteKeyboard;
+ m_pClipboardCarousel = new COptionsClipboardCarousel;
m_pCopyBuffers = new COptionsCopyBuffers;
m_pStats = new COptionsStats;
@@ -63,6 +66,7 @@ COptionsSheet::COptionsSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectP
AddPage(m_pCopyBuffers);
//AddPage(m_pQuickPasteOptions);
AddPage(m_pQuickPasteShortCuts);
+ AddPage(m_pClipboardCarousel);
if(CGetSetOptions::GetAllowFriends())
{
m_pFriends = new COptionFriends;
@@ -86,6 +90,7 @@ COptionsSheet::~COptionsSheet()
delete m_pFriends;
delete m_pUtilites;
delete m_pQuickPasteShortCuts;
+ delete m_pClipboardCarousel;
}
BEGIN_MESSAGE_MAP(COptionsSheet, CPropertySheet)
diff --git a/src/OptionsSheet.h b/src/OptionsSheet.h
index a5cd5bc9..9c5adb42 100644
--- a/src/OptionsSheet.h
+++ b/src/OptionsSheet.h
@@ -56,6 +56,7 @@ class COptionsSheet : public CPropertySheet
CPropertyPage *m_pFriends;
CPropertyPage *m_pCopyBuffers;
CPropertyPage *m_pQuickPasteShortCuts;
+ CPropertyPage *m_pClipboardCarousel;
//CShowTaskBarIcon m_taskBar;
diff --git a/src/SendKeys.cpp b/src/SendKeys.cpp
index 45320481..b7379bd9 100644
--- a/src/SendKeys.cpp
+++ b/src/SendKeys.cpp
@@ -256,8 +256,12 @@ void CSendKeys::AllKeysUp()
SendKeyUp(VK_RSHIFT);
SendKeyUp(VK_LCONTROL);
SendKeyUp(VK_RCONTROL);
- SendKeyUp(VK_LMENU);
- SendKeyUp(VK_RMENU);
+ // An unpaired Alt key-up activates access-key hints in applications such as
+ // Windows 11 Notepad. Only release Alt keys that are actually held.
+ if(GetAsyncKeyState(VK_LMENU) & 0x8000)
+ SendKeyUp(VK_LMENU);
+ if(GetAsyncKeyState(VK_RMENU) & 0x8000)
+ SendKeyUp(VK_RMENU);
}
// Generates KEYUP