From c46186b5d61cadb51144977cb35912efa3130cb5 Mon Sep 17 00:00:00 2001 From: yonka_lap Date: Fri, 31 Jul 2026 22:23:32 +0300 Subject: [PATCH 1/2] Add "Merge Database..." to the quick paste menu Lets you pull the clips out of a second Ditto database into the one you are currently using, instead of having to export and import them a group at a time. MergeDB attaches the other database and copies the Main and Data rows across in one transaction. Every id is shifted above the largest id in the current database so nothing collides, and lParentID is shifted by the same amount so groups keep their children. Clips whose CRC already exists here are skipped, groups always come over, and the four shortcut columns are cleared on the merged rows so they can't fight with the existing shortcuts. Before anything is written the current database is snapshotted with VACUUM INTO to Ditto.db.merge.bak, overwritten on each merge. Co-Authored-By: Claude Fable 5 --- CP_Main.rc | 1 + ReadMe.md | 4 ++ resource.h | 3 +- src/CP_Main.cpp | 124 ++++++++++++++++++++++++++++++++++++++ src/CP_Main.h | 1 + src/DatabaseUtilities.cpp | 85 ++++++++++++++++++++++++++ src/DatabaseUtilities.h | 1 + src/QPasteWnd.cpp | 8 +++ src/QPasteWnd.h | 1 + 9 files changed, 227 insertions(+), 1 deletion(-) diff --git a/CP_Main.rc b/CP_Main.rc index 5a41e2b4..f65f1e2c 100644 --- a/CP_Main.rc +++ b/CP_Main.rc @@ -681,6 +681,7 @@ BEGIN MENUITEM SEPARATOR MENUITEM "Backup Database", ID_MENU_BACKUPDATABASE MENUITEM "Restore Database", ID_MENU_RESTOREDATABSAE + MENUITEM "Merge Database...", ID_MENU_MERGEDATABASE MENUITEM SEPARATOR MENUITEM "Import Clip(s)", ID_MENU_IMPORTCLIP32935 MENUITEM "New Clip", ID_MENU_NEWCLIP32937 diff --git a/ReadMe.md b/ReadMe.md index fc6b43b6..62da1194 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -24,6 +24,10 @@ Ditto is an extension to the standard windows clipboard. It saves each item plac 3. Open Ditto by clicking its icon in the system tray or by pressing its Hot Key which defaults to Ctrl + ` – i.e. hold down Ctrl and press the back-quote (tilde ~) key. 4. Double click or press enter on the item to paste it to the previous window. +## Merging Two Databases + +If you have clips in a second Ditto database, open Ditto, click the menu button in the bottom right corner and choose **Merge Database...**, then pick the other `.db` file. Its clips and groups are copied into the database you are currently using. Clips you already have are skipped, and shortcuts on the incoming clips are cleared so they can't fight with your existing ones. A backup of your current database is saved next to it as `Ditto.db.merge.bak` before anything is changed. + ## Local First - No login - No cloud diff --git a/resource.h b/resource.h index 6ecab036..b31ab453 100644 --- a/resource.h +++ b/resource.h @@ -876,6 +876,7 @@ #define ID_MENU_SHOWSTARREDCLIPS 33000 #define ID_SPECIALPASTE_PASTEASIMAGE 33001 #define ID_MENU_GOTOENTRY 33002 +#define ID_MENU_MERGEDATABASE 33003 // Next default values for new objects // @@ -883,7 +884,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 33004 #define _APS_NEXT_CONTROL_VALUE 2174 #define _APS_NEXT_SYMED_VALUE 104 #endif diff --git a/src/CP_Main.cpp b/src/CP_Main.cpp index a527f98a..0aa7fe7b 100644 --- a/src/CP_Main.cpp +++ b/src/CP_Main.cpp @@ -1046,6 +1046,130 @@ bool CCP_MainApp::ImportClips(HWND hWnd) return true; } +bool CCP_MainApp::MergeDatabase(HWND hWnd) +{ + OPENFILENAME FileName; + TCHAR szFileName[400]; + TCHAR szDir[400]; + + memset(&FileName, 0, sizeof(FileName)); + memset(szFileName, 0, sizeof(szFileName)); + memset(&szDir, 0, sizeof(szDir)); + + CString csInitialDir = CGetSetOptions::GetLastImportDir(); + STRCPY(szDir, csInitialDir); + + FileName.lStructSize = sizeof(FileName); + FileName.lpstrTitle = _T("Merge Database"); + FileName.Flags = OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR; + FileName.nMaxFile = 400; + FileName.lpstrFile = szFileName; + FileName.lpstrInitialDir = szDir; + FileName.lpstrFilter = _T("Ditto Databases (*.db)\0*.db\0\0"); + FileName.lpstrDefExt = _T("db"); + FileName.hwndOwner = hWnd; + + if(GetOpenFileName(&FileName) == 0) + { + return false; + } + + using namespace nsPath; + CPath path(FileName.lpstrFile); + CGetSetOptions::SetLastImportDir(path.GetPath()); + + CString csMergeFrom(FileName.lpstrFile); + CString csCurrentDb = CGetSetOptions::GetDBPath(); + + if(csCurrentDb.CompareNoCase(csMergeFrom) == 0) + { + CShowTaskBarIcon show; + MessageBox(hWnd, theApp.m_Language.GetString("Merge_Same_Db", "Can't merge the current database into itself, select a different database."), _T("Ditto"), MB_OK); + return false; + } + + //also upgrades an older database to the current schema so all the columns exist + if(ValidDB(csMergeFrom) == FALSE) + { + CShowTaskBarIcon show; + MessageBox(hWnd, theApp.m_Language.GetString("Invalid_Database", "Invalid Database"), _T("Ditto"), MB_OK); + return false; + } + + CString csBackup = csCurrentDb + _T(".merge.bak"); + + CString csConfirm; + csConfirm.Format(_T("%s\n\n%s\n\n%s\n%s"), + theApp.m_Language.GetString("Merge_Confirm", "Merge the clips from this database into the current database?"), + csMergeFrom, + theApp.m_Language.GetString("Merge_Confirm_Backup", "A backup of the current database will be saved to:"), + csBackup); + + { + CShowTaskBarIcon show; + if(MessageBox(hWnd, csConfirm, _T("Ditto"), MB_YESNO | MB_ICONQUESTION) != IDYES) + { + return false; + } + } + + int mergedCount = 0; + BOOL bMerged = FALSE; + + { + CWaitCursor wait; + + try + { + //VACUUM INTO writes a consistent snapshot, but it won't write over an existing file + ::DeleteFile(csBackup); + + CString csBackupEscaped = csBackup; + csBackupEscaped.Replace(_T("'"), _T("''")); + + theApp.m_db.execDMLEx(_T("VACUUM INTO '%s'"), csBackupEscaped); + } + catch (CppSQLite3Exception& e) + { + Log(StrF(_T("Merge backup failed: %d - %s"), e.errorCode(), e.errorMessage())); + + CShowTaskBarIcon show; + MessageBox(hWnd, theApp.m_Language.GetString("Merge_Backup_Error", "Could not create a backup of the current database, the merge was canceled."), _T("Ditto"), MB_OK); + return false; + } + + bMerged = MergeDB(csMergeFrom, mergedCount); + } + + CShowTaskBarIcon show; + + if(bMerged) + { + theApp.RefreshView(); + + CString cs; + cs.Format(_T("%s %d "), theApp.m_Language.GetString("Merge_Successfully", "Successfully merged"), mergedCount); + if(mergedCount == 1) + cs += theApp.m_Language.GetString("Clip", "clip"); + else + cs += theApp.m_Language.GetString("Clips", "clips"); + + MessageBox(hWnd, cs, _T("Ditto"), MB_OK); + } + else + { + CString cs; + cs.Format(_T("%s\n\n%s\n%s"), + theApp.m_Language.GetString("Merge_Error", "Error merging the database, no clips were added."), + theApp.m_Language.GetString("Merge_Error_Backup", "A backup of the current database was saved to:"), + csBackup); + + MessageBox(hWnd, cs, _T("Ditto"), MB_OK); + } + + return bMerged == TRUE; +} + void CCP_MainApp::ShowCommandLineError(CString csTitle, CString csMessage) { Log(StrF(_T("ShowCommandLineError %s - %s"), csTitle, csMessage)); diff --git a/src/CP_Main.h b/src/CP_Main.h index 26a6025a..3678a5d4 100644 --- a/src/CP_Main.h +++ b/src/CP_Main.h @@ -107,6 +107,7 @@ class CCP_MainApp : public CWinApp bool ToggleConnectCV(); void UpdateMenuConnectCV(CMenu* pMenu, UINT nMenuID); bool ImportClips(HWND hWnd); + bool MergeDatabase(HWND hWnd); void LoadGlobalClips(); void OnDeleteID(long lID); diff --git a/src/DatabaseUtilities.cpp b/src/DatabaseUtilities.cpp index 3c69d475..6cf26c40 100644 --- a/src/DatabaseUtilities.cpp +++ b/src/DatabaseUtilities.cpp @@ -690,6 +690,91 @@ BOOL RestoreDB(CString backupPath) return ret; } +//Merges the clips from another ditto database into the currently open database. +//Clips whose CRC already exists in this database are skipped, groups are always brought over. +//Shortcuts are cleared on the merged clips so they can't fight with the existing ones. +BOOL MergeDB(CString csPath, int &mergedCount) +{ + mergedCount = 0; + + Log(StrF(_T("Start of MergeDB, from: %s"), csPath)); + + csPath.Replace(_T("'"), _T("''")); + + try + { + theApp.m_db.execDMLEx(_T("ATTACH DATABASE '%s' AS mergeSrc"), csPath); + } + CATCH_SQLITE_EXCEPTION_AND_RETURN(FALSE) + + BOOL ret = FALSE; + + try + { + theApp.m_db.execDML(_T("begin transaction;")); + + theApp.m_db.execDML(_T("DROP TABLE IF EXISTS mergeIds")); + theApp.m_db.execDML(_T("CREATE TEMP TABLE mergeIds AS ") + _T("SELECT lID FROM mergeSrc.Main ") + _T("WHERE bIsGroup = 1 OR CRC IS NULL OR ") + _T("CRC NOT IN (SELECT CRC FROM Main WHERE bIsGroup = 0 AND CRC IS NOT NULL)")); + + //every id from the other db is shifted above the biggest id in this db, that keeps + //the ids unique and lets the group links (lParentID) be shifted by the same amount + int offset = theApp.m_db.execScalar(_T("SELECT IFNULL(MAX(lID), 0) FROM Main")); + + theApp.m_db.execDMLEx(_T("INSERT INTO Main(lID, lDate, mText, lShortCut, lDontAutoDelete, CRC, bIsGroup, lParentID, QuickPasteText, clipOrder, clipGroupOrder, globalShortCut, lastPasteDate, stickyClipOrder, stickyClipGroupOrder, MoveToGroupShortCut, GlobalMoveToGroupShortCut) ") + _T("SELECT lID + %d, lDate, mText, 0, lDontAutoDelete, CRC, bIsGroup, ") + _T("CASE WHEN lParentID < 0 THEN lParentID ELSE lParentID + %d END, ") + _T("QuickPasteText, clipOrder, clipGroupOrder, 0, lastPasteDate, ") + _T("stickyClipOrder, stickyClipGroupOrder, 0, 0 ") + _T("FROM mergeSrc.Main WHERE lID IN (SELECT lID FROM mergeIds)"), offset, offset); + + theApp.m_db.execDMLEx(_T("INSERT INTO Data(lParentID, strClipBoardFormat, ooData) ") + _T("SELECT lParentID + %d, strClipBoardFormat, ooData ") + _T("FROM mergeSrc.Data WHERE lParentID IN (SELECT lID FROM mergeIds)"), offset); + + mergedCount = theApp.m_db.execScalar(_T("SELECT COUNT(*) FROM mergeIds")); + + theApp.m_db.execDML(_T("DROP TABLE mergeIds")); + theApp.m_db.execDML(_T("commit transaction;")); + + ret = TRUE; + } + catch (CppSQLite3Exception& e) + { + Log(StrF(_T("MergeDB error: %d - %s"), e.errorCode(), e.errorMessage())); + + try + { + theApp.m_db.execDML(_T("rollback transaction;")); + } + catch (CppSQLite3Exception&) + { + } + + mergedCount = 0; + } + + try + { + theApp.m_db.execDML(_T("DETACH DATABASE mergeSrc")); + } + catch (CppSQLite3Exception&) + { + } + + if (ret) + { + //both databases can hold the same sticky positions, put them back in order + ReOrderStickyClips(-1, theApp.m_db); + } + + Log(StrF(_T("End of MergeDB, merged: %d, success: %d"), mergedCount, ret)); + + return ret; +} + BOOL CreateDB(CString csFile) { try diff --git a/src/DatabaseUtilities.h b/src/DatabaseUtilities.h index 95410a90..17f4ac6d 100644 --- a/src/DatabaseUtilities.h +++ b/src/DatabaseUtilities.h @@ -34,6 +34,7 @@ BOOL EnsureDirectory(CString csPath); BOOL BackupDB(CString dbPath, CString backupPath); BOOL RestoreDB(CString backupPath); +BOOL MergeDB(CString csPath, int &mergedCount); void ReOrderStickyClips(int parentID, CppSQLite3DB &db); diff --git a/src/QPasteWnd.cpp b/src/QPasteWnd.cpp index 0a9849d1..b0c30764 100644 --- a/src/QPasteWnd.cpp +++ b/src/QPasteWnd.cpp @@ -216,6 +216,7 @@ BEGIN_MESSAGE_MAP(CQPasteWnd, CWndEx) ON_WM_TIMER() ON_COMMAND(ID_MENU_EXPORT, OnMenuExport) ON_COMMAND(ID_MENU_IMPORT, OnMenuImport) + ON_COMMAND(ID_MENU_MERGEDATABASE, OnMenuMergeDatabase) ON_COMMAND(ID_QUICKPROPERTIES_REMOVEQUICKPASTE, OnQuickpropertiesRemovequickpaste) ON_COMMAND(ID_MENU_EDITITEM, OnMenuEdititem) ON_COMMAND(ID_MENU_NEWCLIP, OnMenuNewclip) @@ -2677,6 +2678,13 @@ void CQPasteWnd::OnMenuImport() m_bHideWnd = true; } +void CQPasteWnd::OnMenuMergeDatabase() +{ + m_bHideWnd = false; + theApp.MergeDatabase(m_hWnd); + m_bHideWnd = true; +} + void CQPasteWnd::OnMenuHelp() { CHyperLink::GotoURL(_T("https://github.com/sabrogden/Ditto/wiki"), SW_SHOW); diff --git a/src/QPasteWnd.h b/src/QPasteWnd.h index 2a42b5ec..f3521385 100644 --- a/src/QPasteWnd.h +++ b/src/QPasteWnd.h @@ -466,6 +466,7 @@ class CQPasteWnd: public CWndEx afx_msg void OnTimer(UINT_PTR nIDEvent); afx_msg void OnMenuExport(); afx_msg void OnMenuImport(); + afx_msg void OnMenuMergeDatabase(); afx_msg void OnQuickpropertiesRemovequickpaste(); afx_msg void OnMenuEdititem(); afx_msg void OnMenuNewclip(); From 36d20717483b32a47c534c8aec274c0e9aefca84 Mon Sep 17 00:00:00 2001 From: yonka Date: Sat, 1 Aug 2026 15:55:58 +0300 Subject: [PATCH 2/2] fx --- Addins/DittoUtil/RCa20208 | Bin 0 -> 8400 bytes src/CP_Main.cpp | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 Addins/DittoUtil/RCa20208 diff --git a/Addins/DittoUtil/RCa20208 b/Addins/DittoUtil/RCa20208 new file mode 100644 index 0000000000000000000000000000000000000000..52e5d4ff63db56d1d93566a4f762fe3ed76b9d6b GIT binary patch literal 8400 zcmdU!ZEqS!5Xbj*rGAGiylGTQjloXbcbKPI!3G)ZCQ>A1+1Ry|mB0J{`>TN8y?8yV zuDu>u-E>1Ya$|QUZ5_FRo48Zg(P&%GC$8__YGkDEV|S>LzMg~BaP;)9z+JdUt|xi( z8o!p#X6}RJUrJZ#VJxZN==(@gw-OvRuGs#3I&L8i`)T zW4E$~$o&-S(U9s&^qQ_Kn=_9a$-B&I*1t&Y zXnr2)-Jjm7>#@c4wdO=?&UEj28xJE!rKg>bB9tlby3UrVyKU-9&9 zE#SZ^6bkRnBNGd&yB#lKP5N7fU#vNlE_&Vu_*EP)-iemQI4sFDm)3t>C&y~=aGSl# zKGJ04RY?|+$H$VQ?_8g|m|kB)xGS43$x037BH8#%pI^7g8p#vMWsCDg^4YBK6my#O zI?HN}FL(>wjkM2&N9GGrB|inrKM2R+8q8iQB0k(PLG%2($2@t@z4V@3_S8c}Nb|G_ z&Zp68KSsvqg#G7|elE@4dOd?|aL+v}nc?uc5NV=Fhu4mT?PTi{kMUhSQ+=f05EU7R zT353`qwI`GI9d^+zS8%O|1j6Ep{*$25;knIB2e%8oS`S)j)hkv;Z-EMAGOQ1-k4Ni zMv|qFZy#!>UWUtrj;*hKUOiQAwvf)!GUaFQq-KX)BRzj0&Wvs&o*DQD3Bg2ryPJD! zSy6UQ*ee@t$(;rsuc_4Vt+|hgH#!Gu&Z$o0PI{Qdac|w~9pykf?m(Z0)-?)OoJ-qO zFK}MczUI03^C#KBh5UG`aWcD-qI{^2BdxF+n2}FxQTN>gNZ&Vj73$$64 z{?H)Qs2;=2h5v=d;OifKUy1*s{RZ(WY6Z9t`sP>h32q|AP*lmv?}g~gH5%F08nSe^ zIkP0!OrEkHb1sSJpXFcYD_IL2YZX?%koM@bq%9L1GEc$}LRA7TBwQpp1Ml3^a@9PhFnT0ZXe*M?4FX99!eescj-ER{3QTkw14SKheJuT@XVOCf zG104iQ#TM<(DOlDL(p$}>*=wYI0pT(r+eaQQuEpy@c511^6RZWRK$@gT2>9k$56;M z%#60g*HBa}CZ^{bg=0~pR<<|DY}Cf8c#k|%cO_j@@^!qv^7IThgGSAJ(TH(Q`~+K% zJ+C9*T{0YK>kMvUw7~k(TEZf&n#tlnF|q4qq#h?aQkAfmWk)4FzxFgaH=Zl1&>H#> z%U|lLBAv5SP0z@RzKUrDiY@OqmqI77up*tbr4{ObdJ}z(?CK=$pHJ$=`8k-ft&Z~CHR7J|)TZ=8P^rD7wup(5@1Q|n7l9gX#L z53`dqCTnVG9`#s1rWH=V>32fAL$9g6r%F#S;XVCCG)G5MkETf{@ls=SZW)>BoBqc_ z&vY3>w=tP><PCPH;1jeRRY_N&34N=SWQzUx`ny z>8+L=C{PQQ!AT}(gM6H|oq*W{HjFL5^!8@hTa3)0xOvd?!e%-PSSz|ra`d9i9|`xp zzO32~ne!&K5#2ug4*z8zb_HP{?rtZNMIlyh6B9tdbX4GsvlNLXU7a*Bg#INwvC8Ve zQ~fso&A+b;6-)066od5If+J4YUCK>XKi-+#gvut}NTZYV!HIt9 ze+Rzje=VzfkH+OT{YG0s%}0Ka|9?0Ae|A#s(Z!>`NPGty{*UhBD!z*PiAm03+-%lO cv>lQTqWw;v@cy%i?yIq$p6^npvq`l65A^Cr7ytkO literal 0 HcmV?d00001 diff --git a/src/CP_Main.cpp b/src/CP_Main.cpp index 0aa7fe7b..1c0527db 100644 --- a/src/CP_Main.cpp +++ b/src/CP_Main.cpp @@ -1076,7 +1076,7 @@ bool CCP_MainApp::MergeDatabase(HWND hWnd) using namespace nsPath; CPath path(FileName.lpstrFile); - CGetSetOptions::SetLastImportDir(path.GetPath()); + CGetSetOptions::SetLastImportDir(CString(path.GetPath())); CString csMergeFrom(FileName.lpstrFile); CString csCurrentDb = CGetSetOptions::GetDBPath();