Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Addins/DittoUtil/RCa20208
Binary file not shown.
1 change: 1 addition & 0 deletions CP_Main.rc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion resource.h
Original file line number Diff line number Diff line change
Expand Up @@ -876,14 +876,15 @@
#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
//
#ifdef APSTUDIO_INVOKED
#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
Expand Down
124 changes: 124 additions & 0 deletions src/CP_Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(CString(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));
Expand Down
1 change: 1 addition & 0 deletions src/CP_Main.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
85 changes: 85 additions & 0 deletions src/DatabaseUtilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/DatabaseUtilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions src/QPasteWnd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/QPasteWnd.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down