From e8e92b618cee54aa696e68e002276bf3d9f3e329 Mon Sep 17 00:00:00 2001 From: Yvan Janssens Date: Sun, 22 Mar 2026 21:22:07 +0100 Subject: [PATCH 1/2] Add dependency versions to About dialog --- src/about_dialog.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/about_dialog.cpp b/src/about_dialog.cpp index b346122..4259bab 100644 --- a/src/about_dialog.cpp +++ b/src/about_dialog.cpp @@ -1,7 +1,9 @@ #include "about_dialog.h" +#include #include #include +#include #include #include @@ -10,6 +12,14 @@ namespace { +wxString GetWxWidgetsVersion() { + return wxString::Format("%d.%d.%d", wxMAJOR_VERSION, wxMINOR_VERSION, wxRELEASE_NUMBER); +} + +wxString GetSqliteVersion() { + return wxString::FromUTF8(sqlite3_libversion()); +} + wxBitmap LoadAppBitmap(int size) { wxMemoryInputStream stream(assets_csv_explorer_png, assets_csv_explorer_png_len); wxImage image; @@ -68,6 +78,29 @@ class AboutDialog : public wxDialog { root->Add(details, 1, wxALL | wxALIGN_CENTER_HORIZONTAL, 16); + root->Add(new wxStaticLine(this), 0, wxEXPAND | wxLEFT | wxRIGHT, 16); + + auto* poweredBy = new wxBoxSizer(wxVERTICAL); + + auto* poweredByTitle = new wxStaticText(this, wxID_ANY, "Powered by"); + wxFont sectionFont = poweredByTitle->GetFont(); + sectionFont.MakeBold(); + poweredByTitle->SetFont(sectionFont); + poweredBy->Add(poweredByTitle, 0, wxBOTTOM | wxALIGN_CENTER_HORIZONTAL, 8); + + auto addDependencyLabel = [this, poweredBy](const wxString& name, const wxString& version) { + poweredBy->Add( + new wxStaticText(this, wxID_ANY, wxString::Format("%s %s", name, version), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL), + 0, + wxBOTTOM | wxALIGN_CENTER_HORIZONTAL, + 4); + }; + + addDependencyLabel("wxWidgets", GetWxWidgetsVersion()); + addDependencyLabel("SQLite", GetSqliteVersion()); + + root->Add(poweredBy, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, 16); + auto* closeButton = new wxButton(this, wxID_OK, "Close"); closeButton->SetMinSize(FromDIP(wxSize(140, -1))); root->Add(closeButton, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxALIGN_CENTER_HORIZONTAL, 16); From 538491714e660ee63946033a58da95acc4e3d774 Mon Sep 17 00:00:00 2001 From: Yvan Janssens Date: Sun, 22 Mar 2026 21:29:15 +0100 Subject: [PATCH 2/2] Split windows and dialogs into separate source files --- CMakeLists.txt | 4 + src/print_preview_frame.cpp | 223 ++++++++++++++ src/print_rendering.cpp | 188 ++++++++++++ src/print_support.cpp | 394 +------------------------ src/print_support_internal.h | 14 + src/sqlite_dialog_common.cpp | 405 +++++++++++++++++++++++++ src/sqlite_dialog_common.h | 36 +++ src/sqlite_export_dialog.cpp | 174 +++++++++++ src/sqlite_import_dialog.cpp | 556 +---------------------------------- 9 files changed, 1049 insertions(+), 945 deletions(-) create mode 100644 src/print_preview_frame.cpp create mode 100644 src/print_rendering.cpp create mode 100644 src/print_support_internal.h create mode 100644 src/sqlite_dialog_common.cpp create mode 100644 src/sqlite_dialog_common.h create mode 100644 src/sqlite_export_dialog.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ea9245c..f6901a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,7 +73,11 @@ add_executable(csv_explorer src/go_to_row_dialog.cpp src/main.cpp src/main_frame.cpp + src/print_preview_frame.cpp + src/print_rendering.cpp src/print_support.cpp + src/sqlite_dialog_common.cpp + src/sqlite_export_dialog.cpp src/sqlite_import_dialog.cpp src/unsaved_changes_dialog.cpp ) diff --git a/src/print_preview_frame.cpp b/src/print_preview_frame.cpp new file mode 100644 index 0000000..421264a --- /dev/null +++ b/src/print_preview_frame.cpp @@ -0,0 +1,223 @@ +#include "print_support.h" + +#include "print_support_internal.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +enum { + ID_PREVIEW_PREVIOUS_PAGE = wxID_HIGHEST + 500, + ID_PREVIEW_NEXT_PAGE, + ID_PREVIEW_PORTRAIT, + ID_PREVIEW_LANDSCAPE +}; + +class PrintPreviewPanel final : public wxPanel { +public: + PrintPreviewPanel( + wxWindow* parent, + const PrintableDocument& document, + bool landscape, + std::function pageScrollHandler) + : wxPanel(parent, wxID_ANY), + m_document(document), + m_landscape(landscape), + m_pageScrollHandler(std::move(pageScrollHandler)) { + SetBackgroundStyle(wxBG_STYLE_PAINT); + Bind(wxEVT_PAINT, &PrintPreviewPanel::OnPaint, this); + Bind(wxEVT_MOUSEWHEEL, &PrintPreviewPanel::OnMouseWheel, this); + } + + void SetPageNumber(int pageNumber) { + m_pageNumber = std::max(1, pageNumber); + Refresh(); + } + + void SetLandscape(bool landscape) { + m_landscape = landscape; + Refresh(); + } + +private: + void OnPaint(wxPaintEvent&) { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(wxColour(208, 208, 208))); + dc.Clear(); + + const wxSize client = GetClientSize(); + const wxSize logicalPage = GetLogicalPageSize(m_landscape); + const double scale = std::max( + 0.1, + std::min( + static_cast(std::max(80, client.GetWidth() - 32)) / logicalPage.GetWidth(), + static_cast(std::max(80, client.GetHeight() - 32)) / logicalPage.GetHeight())); + + const wxSize drawnPage( + std::max(1, static_cast(logicalPage.GetWidth() * scale)), + std::max(1, static_cast(logicalPage.GetHeight() * scale))); + const wxRect pageRect( + (client.GetWidth() - drawnPage.GetWidth()) / 2, + (client.GetHeight() - drawnPage.GetHeight()) / 2, + drawnPage.GetWidth(), + drawnPage.GetHeight()); + + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(wxColour(160, 160, 160))); + dc.DrawRectangle(pageRect.x + 8, pageRect.y + 8, pageRect.width, pageRect.height); + + dc.SetBrush(*wxWHITE_BRUSH); + dc.SetPen(*wxBLACK_PEN); + dc.DrawRectangle(pageRect); + + dc.SetClippingRegion(pageRect); + dc.SetDeviceOrigin(pageRect.x, pageRect.y); + dc.SetUserScale(scale, scale); + DrawPage(dc, m_document, m_pageNumber, m_landscape); + } + + void OnMouseWheel(wxMouseEvent& event) { + if (!m_pageScrollHandler) { + event.Skip(); + return; + } + + const int rotation = event.GetWheelRotation(); + if (rotation == 0) { + event.Skip(); + return; + } + + m_pageScrollHandler(rotation > 0 ? -1 : 1); + } + + PrintableDocument m_document; + bool m_landscape{false}; + int m_pageNumber{1}; + std::function m_pageScrollHandler; +}; + +class PrintPreviewFrame final : public wxFrame { +public: + PrintPreviewFrame(wxWindow* parent, const PrintableDocument& document, wxPrintData printData) + : wxFrame(parent, wxID_ANY, wxString::Format("Print Preview - %s", document.title), wxDefaultPosition, wxSize(1100, 800)), + m_document(document), + m_printData(std::move(printData)), + m_landscape(m_printData.GetOrientation() == wxLANDSCAPE) { + if (m_printData.GetOrientation() != wxLANDSCAPE && m_printData.GetOrientation() != wxPORTRAIT) { + m_landscape = GuessLandscapeForPrint(document); + } + + auto* toolbar = CreateToolBar(wxTB_HORIZONTAL | wxTB_TEXT); + toolbar->AddTool(ID_PREVIEW_PREVIOUS_PAGE, "Previous", wxArtProvider::GetBitmapBundle(wxART_GO_BACK, wxART_TOOLBAR)); + toolbar->AddTool(ID_PREVIEW_NEXT_PAGE, "Next", wxArtProvider::GetBitmapBundle(wxART_GO_FORWARD, wxART_TOOLBAR)); + toolbar->AddSeparator(); + toolbar->AddRadioTool(ID_PREVIEW_PORTRAIT, "Portrait", wxArtProvider::GetBitmapBundle(wxART_NORMAL_FILE, wxART_TOOLBAR)); + toolbar->AddRadioTool(ID_PREVIEW_LANDSCAPE, "Landscape", wxArtProvider::GetBitmapBundle(wxART_REPORT_VIEW, wxART_TOOLBAR)); + toolbar->AddSeparator(); + toolbar->AddTool(wxID_PRINT, "Print", wxArtProvider::GetBitmapBundle(wxART_PRINT, wxART_TOOLBAR)); + toolbar->AddSeparator(); + m_pageLabel = new wxStaticText(toolbar, wxID_ANY, {}); + toolbar->AddControl(m_pageLabel); + toolbar->Realize(); + + m_previewPanel = new PrintPreviewPanel( + this, + m_document, + m_landscape, + [this](int delta) { ChangePage(delta); }); + auto* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(m_previewPanel, 1, wxEXPAND, 0); + SetSizer(sizer); + + Bind(wxEVT_TOOL, &PrintPreviewFrame::OnPreviousPage, this, ID_PREVIEW_PREVIOUS_PAGE); + Bind(wxEVT_TOOL, &PrintPreviewFrame::OnNextPage, this, ID_PREVIEW_NEXT_PAGE); + Bind(wxEVT_TOOL, &PrintPreviewFrame::OnPortrait, this, ID_PREVIEW_PORTRAIT); + Bind(wxEVT_TOOL, &PrintPreviewFrame::OnLandscape, this, ID_PREVIEW_LANDSCAPE); + Bind(wxEVT_TOOL, &PrintPreviewFrame::OnPrint, this, wxID_PRINT); + + UpdateOrientation(); + CentreOnParent(); + } + +private: + void UpdateOrientation() { + m_printData.SetOrientation(m_landscape ? wxLANDSCAPE : wxPORTRAIT); + m_pageCount = GetPageCount(m_document, m_landscape); + m_currentPage = std::clamp(m_currentPage, 1, std::max(1, m_pageCount)); + m_previewPanel->SetLandscape(m_landscape); + m_previewPanel->SetPageNumber(m_currentPage); + + auto* toolbar = GetToolBar(); + if (toolbar) { + toolbar->ToggleTool(ID_PREVIEW_PORTRAIT, !m_landscape); + toolbar->ToggleTool(ID_PREVIEW_LANDSCAPE, m_landscape); + toolbar->EnableTool(ID_PREVIEW_PREVIOUS_PAGE, m_currentPage > 1); + toolbar->EnableTool(ID_PREVIEW_NEXT_PAGE, m_currentPage < m_pageCount); + } + + if (m_pageLabel) { + m_pageLabel->SetLabel(wxString::Format("Page %d of %d", m_currentPage, m_pageCount)); + GetToolBar()->Realize(); + } + Layout(); + } + + void OnPreviousPage(wxCommandEvent&) { + ChangePage(-1); + } + + void OnNextPage(wxCommandEvent&) { + ChangePage(1); + } + + void OnPortrait(wxCommandEvent&) { + m_landscape = false; + UpdateOrientation(); + } + + void OnLandscape(wxCommandEvent&) { + m_landscape = true; + UpdateOrientation(); + } + + void OnPrint(wxCommandEvent&) { + ShowPrintDialogForDocument(this, m_document, m_printData); + } + + void ChangePage(int delta) { + const int nextPage = std::clamp(m_currentPage + delta, 1, m_pageCount); + if (nextPage == m_currentPage) { + return; + } + + m_currentPage = nextPage; + UpdateOrientation(); + } + + PrintableDocument m_document; + wxPrintData m_printData; + PrintPreviewPanel* m_previewPanel{nullptr}; + wxStaticText* m_pageLabel{nullptr}; + bool m_landscape{false}; + int m_currentPage{1}; + int m_pageCount{1}; +}; + +} // namespace + +void ShowPrintPreviewWindow(wxWindow* parent, const PrintableDocument& document, wxPrintData& printData) { + if (printData.GetOrientation() != wxLANDSCAPE && printData.GetOrientation() != wxPORTRAIT) { + printData.SetOrientation(GuessLandscapeForPrint(document) ? wxLANDSCAPE : wxPORTRAIT); + } + + auto* frame = new PrintPreviewFrame(parent, document, printData); + frame->Show(); +} diff --git a/src/print_rendering.cpp b/src/print_rendering.cpp new file mode 100644 index 0000000..a69e798 --- /dev/null +++ b/src/print_rendering.cpp @@ -0,0 +1,188 @@ +#include "print_support_internal.h" + +#include +#include +#include + +#include +#include + +namespace { + +struct PrintLayout { + wxFont titleFont; + wxFont headerFont; + wxFont cellFont; + int marginLeft{64}; + int marginTop{64}; + int marginRight{64}; + int marginBottom{64}; + int titleHeight{0}; + int headerHeight{0}; + int rowHeight{0}; + int footerHeight{0}; + int tableTop{0}; + int rowsPerPage{1}; + int pageCount{1}; + std::vector columnWidths; +}; + +void DrawClippedText(wxDC& dc, const wxString& text, const wxRect& rect, int horizontalPadding = 8) { + wxDCClipper clipper(dc, rect); + const int textHeight = dc.GetTextExtent("Ag").GetHeight(); + const int textY = rect.y + std::max(0, (rect.height - textHeight) / 2); + dc.DrawText(text, rect.x + horizontalPadding, textY); +} + +PrintLayout BuildPrintLayout(wxDC& dc, const PrintableDocument& document, const wxSize& pageSize) { + PrintLayout layout; + + const int baseSize = std::max(18, pageSize.GetHeight() / 110); + layout.titleFont = wxFont(wxFontInfo(baseSize + 6).Family(wxFONTFAMILY_SWISS).Bold()); + layout.headerFont = wxFont(wxFontInfo(baseSize + 1).Family(wxFONTFAMILY_SWISS).Bold()); + layout.cellFont = wxFont(wxFontInfo(baseSize).Family(wxFONTFAMILY_SWISS)); + + layout.marginLeft = std::max(48, pageSize.GetWidth() / 18); + layout.marginRight = layout.marginLeft; + layout.marginTop = std::max(48, pageSize.GetHeight() / 20); + layout.marginBottom = std::max(48, pageSize.GetHeight() / 20); + + dc.SetFont(layout.titleFont); + layout.titleHeight = dc.GetTextExtent(document.title.IsEmpty() ? "CSV Explorer" : document.title).GetHeight() + 20; + + dc.SetFont(layout.headerFont); + layout.headerHeight = dc.GetTextExtent("Ag").GetHeight() + 22; + + dc.SetFont(layout.cellFont); + layout.rowHeight = dc.GetTextExtent("Ag").GetHeight() + 18; + layout.footerHeight = dc.GetTextExtent("Page 999").GetHeight() + 16; + + layout.tableTop = layout.marginTop + layout.titleHeight + 12; + + const unsigned int columnCount = GetColumnCount(document); + const int availableWidth = std::max(120, pageSize.GetWidth() - layout.marginLeft - layout.marginRight); + + layout.columnWidths.resize(columnCount, 80); + int totalWidth = 0; + for (unsigned int col = 0; col < columnCount; ++col) { + dc.SetFont(layout.headerFont); + int width = dc.GetTextExtent(GetHeaderTitle(document, static_cast(col))).GetWidth() + 26; + + dc.SetFont(layout.cellFont); + const unsigned int sampleRows = std::min(static_cast(document.rows.size()), 40u); + for (unsigned int row = 0; row < sampleRows; ++row) { + width = std::max(width, dc.GetTextExtent(GetCellText(document, row, col)).GetWidth() + 26); + } + + width = std::clamp(width, 70, 260); + layout.columnWidths[col] = width; + totalWidth += width; + } + + if (totalWidth > availableWidth && totalWidth > 0) { + const double scale = static_cast(availableWidth) / static_cast(totalWidth); + int adjustedTotal = 0; + for (int& width : layout.columnWidths) { + width = std::max(44, static_cast(std::floor(width * scale))); + adjustedTotal += width; + } + + int remaining = availableWidth - adjustedTotal; + for (int i = 0; remaining > 0 && !layout.columnWidths.empty(); ++i, --remaining) { + layout.columnWidths[static_cast(i) % layout.columnWidths.size()] += 1; + } + } + + const int availableHeight = std::max( + layout.rowHeight, + pageSize.GetHeight() - layout.tableTop - layout.marginBottom - layout.footerHeight - layout.headerHeight); + layout.rowsPerPage = std::max(1, availableHeight / layout.rowHeight); + + const int totalRows = std::max(1, static_cast(document.rows.size())); + layout.pageCount = std::max(1, static_cast(std::ceil(static_cast(totalRows) / layout.rowsPerPage))); + return layout; +} + +} // namespace + +wxSize GetLogicalPageSize(bool landscape) { + const wxSize portrait(1240, 1754); + return landscape ? wxSize(portrait.GetHeight(), portrait.GetWidth()) : portrait; +} + +wxString GetHeaderTitle(const PrintableDocument& document, int col) { + if (col >= 0 && col < static_cast(document.headers.size()) && !document.headers[col].IsEmpty()) { + return document.headers[col]; + } + return wxString::Format("Column %d", col + 1); +} + +unsigned int GetColumnCount(const PrintableDocument& document) { + unsigned int columnCount = static_cast(document.headers.size()); + for (const auto& row : document.rows) { + columnCount = std::max(columnCount, static_cast(row.size())); + } + return std::max(columnCount, 1u); +} + +wxString GetCellText(const PrintableDocument& document, unsigned int row, unsigned int col) { + if (row < document.rows.size() && col < document.rows[row].size()) { + return document.rows[row][col]; + } + return {}; +} + +void DrawPage(wxDC& dc, const PrintableDocument& document, int pageNumber, bool landscape) { + const wxSize pageSize = GetLogicalPageSize(landscape); + const PrintLayout layout = BuildPrintLayout(dc, document, pageSize); + + dc.SetBackground(wxBrush(*wxWHITE)); + dc.Clear(); + + dc.SetTextForeground(*wxBLACK); + dc.SetPen(*wxBLACK_PEN); + + dc.SetFont(layout.titleFont); + dc.DrawText(document.title.IsEmpty() ? "untitled.csv" : document.title, layout.marginLeft, layout.marginTop); + + int x = layout.marginLeft; + int y = layout.tableTop; + + dc.SetFont(layout.headerFont); + dc.SetBrush(wxBrush(wxColour(240, 240, 240))); + for (size_t col = 0; col < layout.columnWidths.size(); ++col) { + const wxRect rect(x, y, layout.columnWidths[col], layout.headerHeight); + dc.DrawRectangle(rect); + DrawClippedText(dc, GetHeaderTitle(document, static_cast(col)), rect); + x += layout.columnWidths[col]; + } + + dc.SetFont(layout.cellFont); + dc.SetBrush(*wxWHITE_BRUSH); + + const int startRow = (pageNumber - 1) * layout.rowsPerPage; + const int endRow = std::min(startRow + layout.rowsPerPage, std::max(1, static_cast(document.rows.size()))); + y += layout.headerHeight; + + for (int row = startRow; row < endRow; ++row) { + x = layout.marginLeft; + for (size_t col = 0; col < layout.columnWidths.size(); ++col) { + const wxRect rect(x, y, layout.columnWidths[col], layout.rowHeight); + dc.DrawRectangle(rect); + DrawClippedText(dc, GetCellText(document, static_cast(row), static_cast(col)), rect); + x += layout.columnWidths[col]; + } + y += layout.rowHeight; + } + + dc.SetFont(layout.cellFont); + const wxString footer = wxString::Format("Page %d of %d", pageNumber, layout.pageCount); + const int footerY = pageSize.GetHeight() - layout.marginBottom + 8; + dc.DrawText(footer, layout.marginLeft, footerY); +} + +int GetPageCount(const PrintableDocument& document, bool landscape) { + wxBitmap bitmap(8, 8); + wxMemoryDC dc(bitmap); + return BuildPrintLayout(dc, document, GetLogicalPageSize(landscape)).pageCount; +} diff --git a/src/print_support.cpp b/src/print_support.cpp index 3c1fd9e..1825268 100644 --- a/src/print_support.cpp +++ b/src/print_support.cpp @@ -1,203 +1,13 @@ #include "print_support.h" +#include "print_support_internal.h" + #include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include namespace { -enum { - ID_PREVIEW_PREVIOUS_PAGE = wxID_HIGHEST + 500, - ID_PREVIEW_NEXT_PAGE, - ID_PREVIEW_PORTRAIT, - ID_PREVIEW_LANDSCAPE -}; - -wxSize GetLogicalPageSize(bool landscape) { - const wxSize portrait(1240, 1754); - return landscape ? wxSize(portrait.GetHeight(), portrait.GetWidth()) : portrait; -} - -struct PrintLayout { - wxFont titleFont; - wxFont headerFont; - wxFont cellFont; - int marginLeft{64}; - int marginTop{64}; - int marginRight{64}; - int marginBottom{64}; - int titleHeight{0}; - int headerHeight{0}; - int rowHeight{0}; - int footerHeight{0}; - int tableTop{0}; - int rowsPerPage{1}; - int pageCount{1}; - std::vector columnWidths; -}; - -wxString GetHeaderTitle(const PrintableDocument& document, int col) { - if (col >= 0 && col < static_cast(document.headers.size()) && !document.headers[col].IsEmpty()) { - return document.headers[col]; - } - return wxString::Format("Column %d", col + 1); -} - -unsigned int GetColumnCount(const PrintableDocument& document) { - unsigned int columnCount = static_cast(document.headers.size()); - for (const auto& row : document.rows) { - columnCount = std::max(columnCount, static_cast(row.size())); - } - return std::max(columnCount, 1u); -} - -wxString GetCellText(const PrintableDocument& document, unsigned int row, unsigned int col) { - if (row < document.rows.size() && col < document.rows[row].size()) { - return document.rows[row][col]; - } - return {}; -} - -void DrawClippedText(wxDC& dc, const wxString& text, const wxRect& rect, int horizontalPadding = 8) { - wxDCClipper clipper(dc, rect); - const int textHeight = dc.GetTextExtent("Ag").GetHeight(); - const int textY = rect.y + std::max(0, (rect.height - textHeight) / 2); - dc.DrawText(text, rect.x + horizontalPadding, textY); -} - -PrintLayout BuildPrintLayout(wxDC& dc, const PrintableDocument& document, const wxSize& pageSize) { - PrintLayout layout; - - const int baseSize = std::max(18, pageSize.GetHeight() / 110); - layout.titleFont = wxFont(wxFontInfo(baseSize + 6).Family(wxFONTFAMILY_SWISS).Bold()); - layout.headerFont = wxFont(wxFontInfo(baseSize + 1).Family(wxFONTFAMILY_SWISS).Bold()); - layout.cellFont = wxFont(wxFontInfo(baseSize).Family(wxFONTFAMILY_SWISS)); - - layout.marginLeft = std::max(48, pageSize.GetWidth() / 18); - layout.marginRight = layout.marginLeft; - layout.marginTop = std::max(48, pageSize.GetHeight() / 20); - layout.marginBottom = std::max(48, pageSize.GetHeight() / 20); - - dc.SetFont(layout.titleFont); - layout.titleHeight = dc.GetTextExtent(document.title.IsEmpty() ? "CSV Explorer" : document.title).GetHeight() + 20; - - dc.SetFont(layout.headerFont); - layout.headerHeight = dc.GetTextExtent("Ag").GetHeight() + 22; - - dc.SetFont(layout.cellFont); - layout.rowHeight = dc.GetTextExtent("Ag").GetHeight() + 18; - layout.footerHeight = dc.GetTextExtent("Page 999").GetHeight() + 16; - - layout.tableTop = layout.marginTop + layout.titleHeight + 12; - - const unsigned int columnCount = GetColumnCount(document); - const int availableWidth = std::max(120, pageSize.GetWidth() - layout.marginLeft - layout.marginRight); - - layout.columnWidths.resize(columnCount, 80); - int totalWidth = 0; - for (unsigned int col = 0; col < columnCount; ++col) { - dc.SetFont(layout.headerFont); - int width = dc.GetTextExtent(GetHeaderTitle(document, static_cast(col))).GetWidth() + 26; - - dc.SetFont(layout.cellFont); - const unsigned int sampleRows = std::min(static_cast(document.rows.size()), 40u); - for (unsigned int row = 0; row < sampleRows; ++row) { - width = std::max(width, dc.GetTextExtent(GetCellText(document, row, col)).GetWidth() + 26); - } - - width = std::clamp(width, 70, 260); - layout.columnWidths[col] = width; - totalWidth += width; - } - - if (totalWidth > availableWidth && totalWidth > 0) { - const double scale = static_cast(availableWidth) / static_cast(totalWidth); - int adjustedTotal = 0; - for (int& width : layout.columnWidths) { - width = std::max(44, static_cast(std::floor(width * scale))); - adjustedTotal += width; - } - - int remaining = availableWidth - adjustedTotal; - for (int i = 0; remaining > 0 && !layout.columnWidths.empty(); ++i, --remaining) { - layout.columnWidths[static_cast(i) % layout.columnWidths.size()] += 1; - } - } - - const int availableHeight = std::max( - layout.rowHeight, - pageSize.GetHeight() - layout.tableTop - layout.marginBottom - layout.footerHeight - layout.headerHeight); - layout.rowsPerPage = std::max(1, availableHeight / layout.rowHeight); - - const int totalRows = std::max(1, static_cast(document.rows.size())); - layout.pageCount = std::max(1, static_cast(std::ceil(static_cast(totalRows) / layout.rowsPerPage))); - return layout; -} - -void DrawPage(wxDC& dc, const PrintableDocument& document, int pageNumber, bool landscape) { - const wxSize pageSize = GetLogicalPageSize(landscape); - const PrintLayout layout = BuildPrintLayout(dc, document, pageSize); - - dc.SetBackground(wxBrush(*wxWHITE)); - dc.Clear(); - - dc.SetTextForeground(*wxBLACK); - dc.SetPen(*wxBLACK_PEN); - - dc.SetFont(layout.titleFont); - dc.DrawText(document.title.IsEmpty() ? "untitled.csv" : document.title, layout.marginLeft, layout.marginTop); - - int x = layout.marginLeft; - int y = layout.tableTop; - - dc.SetFont(layout.headerFont); - dc.SetBrush(wxBrush(wxColour(240, 240, 240))); - for (size_t col = 0; col < layout.columnWidths.size(); ++col) { - const wxRect rect(x, y, layout.columnWidths[col], layout.headerHeight); - dc.DrawRectangle(rect); - DrawClippedText(dc, GetHeaderTitle(document, static_cast(col)), rect); - x += layout.columnWidths[col]; - } - - dc.SetFont(layout.cellFont); - dc.SetBrush(*wxWHITE_BRUSH); - - const int startRow = (pageNumber - 1) * layout.rowsPerPage; - const int endRow = std::min(startRow + layout.rowsPerPage, std::max(1, static_cast(document.rows.size()))); - y += layout.headerHeight; - - for (int row = startRow; row < endRow; ++row) { - x = layout.marginLeft; - for (size_t col = 0; col < layout.columnWidths.size(); ++col) { - const wxRect rect(x, y, layout.columnWidths[col], layout.rowHeight); - dc.DrawRectangle(rect); - DrawClippedText(dc, GetCellText(document, static_cast(row), static_cast(col)), rect); - x += layout.columnWidths[col]; - } - y += layout.rowHeight; - } - - dc.SetFont(layout.cellFont); - const wxString footer = wxString::Format("Page %d of %d", pageNumber, layout.pageCount); - const int footerY = pageSize.GetHeight() - layout.marginBottom + 8; - dc.DrawText(footer, layout.marginLeft, footerY); -} - -int GetPageCount(const PrintableDocument& document, bool landscape) { - wxBitmap bitmap(8, 8); - wxMemoryDC dc(bitmap); - return BuildPrintLayout(dc, document, GetLogicalPageSize(landscape)).pageCount; -} - class CsvPrintout final : public wxPrintout { public: CsvPrintout(const PrintableDocument& document, bool landscape) @@ -243,197 +53,6 @@ class CsvPrintout final : public wxPrintout { bool m_landscape{false}; }; -class PrintPreviewPanel final : public wxPanel { -public: - PrintPreviewPanel( - wxWindow* parent, - const PrintableDocument& document, - bool landscape, - std::function pageScrollHandler) - : wxPanel(parent, wxID_ANY), - m_document(document), - m_landscape(landscape), - m_pageScrollHandler(std::move(pageScrollHandler)) { - SetBackgroundStyle(wxBG_STYLE_PAINT); - Bind(wxEVT_PAINT, &PrintPreviewPanel::OnPaint, this); - Bind(wxEVT_MOUSEWHEEL, &PrintPreviewPanel::OnMouseWheel, this); - } - - void SetPageNumber(int pageNumber) { - m_pageNumber = std::max(1, pageNumber); - Refresh(); - } - - void SetLandscape(bool landscape) { - m_landscape = landscape; - Refresh(); - } - -private: - void OnPaint(wxPaintEvent&) { - wxAutoBufferedPaintDC dc(this); - dc.SetBackground(wxBrush(wxColour(208, 208, 208))); - dc.Clear(); - - const wxSize client = GetClientSize(); - const wxSize logicalPage = GetLogicalPageSize(m_landscape); - const double scale = std::max( - 0.1, - std::min( - static_cast(std::max(80, client.GetWidth() - 32)) / logicalPage.GetWidth(), - static_cast(std::max(80, client.GetHeight() - 32)) / logicalPage.GetHeight())); - - const wxSize drawnPage( - std::max(1, static_cast(logicalPage.GetWidth() * scale)), - std::max(1, static_cast(logicalPage.GetHeight() * scale))); - const wxRect pageRect( - (client.GetWidth() - drawnPage.GetWidth()) / 2, - (client.GetHeight() - drawnPage.GetHeight()) / 2, - drawnPage.GetWidth(), - drawnPage.GetHeight()); - - dc.SetPen(*wxTRANSPARENT_PEN); - dc.SetBrush(wxBrush(wxColour(160, 160, 160))); - dc.DrawRectangle(pageRect.x + 8, pageRect.y + 8, pageRect.width, pageRect.height); - - dc.SetBrush(*wxWHITE_BRUSH); - dc.SetPen(*wxBLACK_PEN); - dc.DrawRectangle(pageRect); - - dc.SetClippingRegion(pageRect); - dc.SetDeviceOrigin(pageRect.x, pageRect.y); - dc.SetUserScale(scale, scale); - DrawPage(dc, m_document, m_pageNumber, m_landscape); - } - - void OnMouseWheel(wxMouseEvent& event) { - if (!m_pageScrollHandler) { - event.Skip(); - return; - } - - const int rotation = event.GetWheelRotation(); - if (rotation == 0) { - event.Skip(); - return; - } - - m_pageScrollHandler(rotation > 0 ? -1 : 1); - } - - PrintableDocument m_document; - bool m_landscape{false}; - int m_pageNumber{1}; - std::function m_pageScrollHandler; -}; - -class PrintPreviewFrame final : public wxFrame { -public: - PrintPreviewFrame(wxWindow* parent, const PrintableDocument& document, wxPrintData printData) - : wxFrame(parent, wxID_ANY, wxString::Format("Print Preview - %s", document.title), wxDefaultPosition, wxSize(1100, 800)), - m_document(document), - m_printData(std::move(printData)), - m_landscape(m_printData.GetOrientation() == wxLANDSCAPE) { - if (m_printData.GetOrientation() != wxLANDSCAPE && m_printData.GetOrientation() != wxPORTRAIT) { - m_landscape = GuessLandscapeForPrint(document); - } - - auto* toolbar = CreateToolBar(wxTB_HORIZONTAL | wxTB_TEXT); - toolbar->AddTool(ID_PREVIEW_PREVIOUS_PAGE, "Previous", wxArtProvider::GetBitmapBundle(wxART_GO_BACK, wxART_TOOLBAR)); - toolbar->AddTool(ID_PREVIEW_NEXT_PAGE, "Next", wxArtProvider::GetBitmapBundle(wxART_GO_FORWARD, wxART_TOOLBAR)); - toolbar->AddSeparator(); - toolbar->AddRadioTool(ID_PREVIEW_PORTRAIT, "Portrait", wxArtProvider::GetBitmapBundle(wxART_NORMAL_FILE, wxART_TOOLBAR)); - toolbar->AddRadioTool(ID_PREVIEW_LANDSCAPE, "Landscape", wxArtProvider::GetBitmapBundle(wxART_REPORT_VIEW, wxART_TOOLBAR)); - toolbar->AddSeparator(); - toolbar->AddTool(wxID_PRINT, "Print", wxArtProvider::GetBitmapBundle(wxART_PRINT, wxART_TOOLBAR)); - toolbar->AddSeparator(); - m_pageLabel = new wxStaticText(toolbar, wxID_ANY, {}); - toolbar->AddControl(m_pageLabel); - toolbar->Realize(); - - m_previewPanel = new PrintPreviewPanel( - this, - m_document, - m_landscape, - [this](int delta) { ChangePage(delta); }); - auto* sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(m_previewPanel, 1, wxEXPAND, 0); - SetSizer(sizer); - - Bind(wxEVT_TOOL, &PrintPreviewFrame::OnPreviousPage, this, ID_PREVIEW_PREVIOUS_PAGE); - Bind(wxEVT_TOOL, &PrintPreviewFrame::OnNextPage, this, ID_PREVIEW_NEXT_PAGE); - Bind(wxEVT_TOOL, &PrintPreviewFrame::OnPortrait, this, ID_PREVIEW_PORTRAIT); - Bind(wxEVT_TOOL, &PrintPreviewFrame::OnLandscape, this, ID_PREVIEW_LANDSCAPE); - Bind(wxEVT_TOOL, &PrintPreviewFrame::OnPrint, this, wxID_PRINT); - - UpdateOrientation(); - CentreOnParent(); - } - -private: - void UpdateOrientation() { - m_printData.SetOrientation(m_landscape ? wxLANDSCAPE : wxPORTRAIT); - m_pageCount = GetPageCount(m_document, m_landscape); - m_currentPage = std::clamp(m_currentPage, 1, std::max(1, m_pageCount)); - m_previewPanel->SetLandscape(m_landscape); - m_previewPanel->SetPageNumber(m_currentPage); - - auto* toolbar = GetToolBar(); - if (toolbar) { - toolbar->ToggleTool(ID_PREVIEW_PORTRAIT, !m_landscape); - toolbar->ToggleTool(ID_PREVIEW_LANDSCAPE, m_landscape); - toolbar->EnableTool(ID_PREVIEW_PREVIOUS_PAGE, m_currentPage > 1); - toolbar->EnableTool(ID_PREVIEW_NEXT_PAGE, m_currentPage < m_pageCount); - } - - if (m_pageLabel) { - m_pageLabel->SetLabel(wxString::Format("Page %d of %d", m_currentPage, m_pageCount)); - GetToolBar()->Realize(); - } - Layout(); - } - - void OnPreviousPage(wxCommandEvent&) { - ChangePage(-1); - } - - void OnNextPage(wxCommandEvent&) { - ChangePage(1); - } - - void OnPortrait(wxCommandEvent&) { - m_landscape = false; - UpdateOrientation(); - } - - void OnLandscape(wxCommandEvent&) { - m_landscape = true; - UpdateOrientation(); - } - - void OnPrint(wxCommandEvent&) { - ShowPrintDialogForDocument(this, m_document, m_printData); - } - - void ChangePage(int delta) { - const int nextPage = std::clamp(m_currentPage + delta, 1, m_pageCount); - if (nextPage == m_currentPage) { - return; - } - - m_currentPage = nextPage; - UpdateOrientation(); - } - - PrintableDocument m_document; - wxPrintData m_printData; - PrintPreviewPanel* m_previewPanel{nullptr}; - wxStaticText* m_pageLabel{nullptr}; - bool m_landscape{false}; - int m_currentPage{1}; - int m_pageCount{1}; -}; - } // namespace bool GuessLandscapeForPrint(const PrintableDocument& document) { @@ -470,12 +89,3 @@ bool ShowPrintDialogForDocument(wxWindow* parent, const PrintableDocument& docum } return printed; } - -void ShowPrintPreviewWindow(wxWindow* parent, const PrintableDocument& document, wxPrintData& printData) { - if (printData.GetOrientation() != wxLANDSCAPE && printData.GetOrientation() != wxPORTRAIT) { - printData.SetOrientation(GuessLandscapeForPrint(document) ? wxLANDSCAPE : wxPORTRAIT); - } - - auto* frame = new PrintPreviewFrame(parent, document, printData); - frame->Show(); -} diff --git a/src/print_support_internal.h b/src/print_support_internal.h new file mode 100644 index 0000000..f6caddf --- /dev/null +++ b/src/print_support_internal.h @@ -0,0 +1,14 @@ +#pragma once + +#include "print_support.h" + +#include +#include +#include + +wxSize GetLogicalPageSize(bool landscape); +wxString GetHeaderTitle(const PrintableDocument& document, int col); +unsigned int GetColumnCount(const PrintableDocument& document); +wxString GetCellText(const PrintableDocument& document, unsigned int row, unsigned int col); +void DrawPage(wxDC& dc, const PrintableDocument& document, int pageNumber, bool landscape); +int GetPageCount(const PrintableDocument& document, bool landscape); diff --git a/src/sqlite_dialog_common.cpp b/src/sqlite_dialog_common.cpp new file mode 100644 index 0000000..0d6a1b6 --- /dev/null +++ b/src/sqlite_dialog_common.cpp @@ -0,0 +1,405 @@ +#include "sqlite_dialog_common.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace sqlite_dialog { + +wxString SqliteError(sqlite3* db, const wxString& fallback) { + if (!db) { + return fallback; + } + + const char* message = sqlite3_errmsg(db); + if (!message || *message == '\0') { + return fallback; + } + + return wxString::FromUTF8(message); +} + +wxString QuoteSqlIdentifier(const wxString& identifier) { + wxString quoted = identifier; + quoted.Replace("\"", "\"\""); + return "\"" + quoted + "\""; +} + +wxString BlobToHexString(const void* blob, int size) { + if (!blob || size <= 0) { + return {}; + } + + const unsigned char* bytes = static_cast(blob); + wxString hex = "0x"; + static constexpr char digits[] = "0123456789ABCDEF"; + for (int i = 0; i < size; ++i) { + hex += digits[bytes[i] >> 4]; + hex += digits[bytes[i] & 0x0F]; + } + return hex; +} + +bool OpenSqliteDatabase(const wxString& path, int flags, sqlite3** outDb, wxString* errorMessage) { + *outDb = nullptr; + const wxCharBuffer utf8Path = path.utf8_str(); + const int result = sqlite3_open_v2(utf8Path.data(), outDb, flags, nullptr); + if (result == SQLITE_OK) { + return true; + } + + if (errorMessage) { + *errorMessage = SqliteError(*outDb, "Unable to open SQLite database."); + } + if (*outDb) { + sqlite3_close(*outDb); + *outDb = nullptr; + } + return false; +} + +bool LoadSqliteTableNames(const wxString& path, std::vector* tableNames, wxString* errorMessage) { + tableNames->clear(); + + sqlite3* db = nullptr; + if (!OpenSqliteDatabase(path, SQLITE_OPEN_READONLY, &db, errorMessage)) { + return false; + } + + static constexpr const char* query = + "SELECT name " + "FROM sqlite_master " + "WHERE type = 'table' AND name NOT LIKE 'sqlite_%' " + "ORDER BY name"; + + sqlite3_stmt* statement = nullptr; + const int prepareResult = sqlite3_prepare_v2(db, query, -1, &statement, nullptr); + if (prepareResult != SQLITE_OK) { + if (errorMessage) { + *errorMessage = SqliteError(db, "Unable to read table list from SQLite database."); + } + sqlite3_close(db); + return false; + } + + while (sqlite3_step(statement) == SQLITE_ROW) { + const unsigned char* text = sqlite3_column_text(statement, 0); + if (text) { + tableNames->push_back(wxString::FromUTF8(reinterpret_cast(text))); + } + } + + sqlite3_finalize(statement); + sqlite3_close(db); + return true; +} + +bool ReadSqliteTable(const wxString& path, const wxString& tableName, ImportedSqliteTable* importedTable, wxString* errorMessage) { + importedTable->documentName = tableName + ".csv"; + importedTable->headers.clear(); + importedTable->rows.clear(); + + sqlite3* db = nullptr; + if (!OpenSqliteDatabase(path, SQLITE_OPEN_READONLY, &db, errorMessage)) { + return false; + } + + const wxString query = "SELECT * FROM " + QuoteSqlIdentifier(tableName); + sqlite3_stmt* statement = nullptr; + const wxCharBuffer utf8Query = query.utf8_str(); + const int prepareResult = sqlite3_prepare_v2(db, utf8Query.data(), -1, &statement, nullptr); + if (prepareResult != SQLITE_OK) { + if (errorMessage) { + *errorMessage = SqliteError(db, "Unable to read selected SQLite table."); + } + sqlite3_close(db); + return false; + } + + const int columnCount = sqlite3_column_count(statement); + importedTable->headers.reserve(static_cast(columnCount)); + for (int col = 0; col < columnCount; ++col) { + const char* name = sqlite3_column_name(statement, col); + importedTable->headers.push_back(name ? wxString::FromUTF8(name) : wxString::Format("Column %d", col + 1)); + } + + int stepResult = SQLITE_ROW; + while ((stepResult = sqlite3_step(statement)) == SQLITE_ROW) { + std::vector row; + row.reserve(static_cast(columnCount)); + for (int col = 0; col < columnCount; ++col) { + switch (sqlite3_column_type(statement, col)) { + case SQLITE_NULL: + row.emplace_back(); + break; + case SQLITE_BLOB: + row.push_back(BlobToHexString(sqlite3_column_blob(statement, col), sqlite3_column_bytes(statement, col))); + break; + default: { + const unsigned char* value = sqlite3_column_text(statement, col); + row.push_back(value ? wxString::FromUTF8(reinterpret_cast(value)) : wxString()); + break; + } + } + } + importedTable->rows.push_back(std::move(row)); + } + + sqlite3_finalize(statement); + sqlite3_close(db); + + if (stepResult != SQLITE_DONE) { + if (errorMessage) { + *errorMessage = "Unable to finish reading the selected SQLite table."; + } + return false; + } + + return true; +} + +wxString SuggestedTableName(const wxString& documentName) { + wxString tableName = wxFileName(documentName).GetName(); + if (tableName.IsEmpty()) { + tableName = documentName; + } + if (tableName.IsEmpty()) { + tableName = "imported_data"; + } + + for (size_t i = 0; i < tableName.Length(); ++i) { + const wxUniChar ch = tableName[i]; + if (!(wxIsalnum(ch) || ch == '_')) { + tableName[i] = '_'; + } + } + + if (tableName.IsEmpty() || wxIsdigit(tableName[0])) { + tableName.Prepend("table_"); + } + return tableName; +} + +std::vector BuildExportColumnNames(const ImportedSqliteTable& table) { + const size_t columnCount = table.headers.size(); + std::vector names; + names.reserve(columnCount); + + for (size_t i = 0; i < columnCount; ++i) { + wxString name = table.headers[i]; + if (name.IsEmpty()) { + name = wxString::Format("Column_%zu", i + 1); + } + + for (size_t j = 0; j < name.Length(); ++j) { + const wxUniChar ch = name[j]; + if (!(wxIsalnum(ch) || ch == '_')) { + name[j] = '_'; + } + } + + if (name.IsEmpty() || wxIsdigit(name[0])) { + name.Prepend("Column_"); + } + + if (name.CmpNoCase("ID") == 0) { + name += "_value"; + } + + const wxString baseName = name; + int suffix = 2; + while (std::any_of(names.begin(), names.end(), [&name](const wxString& existing) { + return existing.CmpNoCase(name) == 0; + })) { + name = wxString::Format("%s_%d", baseName, suffix++); + } + + names.push_back(name); + } + + return names; +} + +bool ExecuteSql(sqlite3* db, const wxString& sql, wxString* errorMessage) { + char* rawError = nullptr; + const wxCharBuffer utf8Sql = sql.utf8_str(); + const int result = sqlite3_exec(db, utf8Sql.data(), nullptr, nullptr, &rawError); + if (result == SQLITE_OK) { + return true; + } + + if (errorMessage) { + wxString sqliteMessage = rawError ? wxString::FromUTF8(rawError) : SqliteError(db, "SQLite command failed."); + *errorMessage = wxString::Format("%s\n\nSQL:\n%s", sqliteMessage, sql); + } + if (rawError) { + sqlite3_free(rawError); + } + return false; +} + +bool BindSqlValue(sqlite3_stmt* statement, int index, const wxString& typeName, const wxString& value, wxString* errorMessage) { + if (value.IsEmpty()) { + return sqlite3_bind_null(statement, index) == SQLITE_OK; + } + + if (typeName == "INTEGER") { + long long integerValue = 0; + if (value.ToLongLong(&integerValue)) { + return sqlite3_bind_int64(statement, index, static_cast(integerValue)) == SQLITE_OK; + } + } else if (typeName == "REAL") { + double realValue = 0.0; + if (value.ToDouble(&realValue)) { + return sqlite3_bind_double(statement, index, realValue) == SQLITE_OK; + } + } else if (typeName == "NUMERIC") { + long long integerValue = 0; + if (value.ToLongLong(&integerValue)) { + return sqlite3_bind_int64(statement, index, static_cast(integerValue)) == SQLITE_OK; + } + double realValue = 0.0; + if (value.ToDouble(&realValue)) { + return sqlite3_bind_double(statement, index, realValue) == SQLITE_OK; + } + } else if (typeName == "BLOB") { + const wxCharBuffer utf8Value = value.utf8_str(); + return sqlite3_bind_blob(statement, index, utf8Value.data(), static_cast(strlen(utf8Value.data())), SQLITE_TRANSIENT) == SQLITE_OK; + } + + const wxCharBuffer utf8Value = value.utf8_str(); + const int result = sqlite3_bind_text(statement, index, utf8Value.data(), -1, SQLITE_TRANSIENT); + if (result != SQLITE_OK && errorMessage) { + *errorMessage = "Unable to bind a value for SQLite export."; + } + return result == SQLITE_OK; +} + +bool WriteSqliteTable( + wxWindow* parent, + const wxString& path, + const wxString& tableName, + const std::vector& columnNames, + const std::vector& columnTypes, + const std::vector>& rows, + wxString* errorMessage) { + sqlite3* db = nullptr; + if (!OpenSqliteDatabase(path, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &db, errorMessage)) { + return false; + } + + wxString createSql = "CREATE TABLE " + QuoteSqlIdentifier(tableName) + " (" + + QuoteSqlIdentifier("ID") + " INTEGER PRIMARY KEY AUTOINCREMENT"; + for (size_t i = 0; i < columnNames.size(); ++i) { + createSql += ", " + QuoteSqlIdentifier(columnNames[i]) + " " + columnTypes[i]; + } + createSql += ")"; + + if (!ExecuteSql(db, createSql, errorMessage)) { + sqlite3_close(db); + return false; + } + + wxString insertSql = "INSERT INTO " + QuoteSqlIdentifier(tableName); + if (!columnNames.empty()) { + insertSql += " ("; + for (size_t i = 0; i < columnNames.size(); ++i) { + if (i > 0) { + insertSql += ", "; + } + insertSql += QuoteSqlIdentifier(columnNames[i]); + } + insertSql += ")"; + } + insertSql += " VALUES ("; + for (size_t i = 0; i < columnNames.size(); ++i) { + if (i > 0) { + insertSql += ", "; + } + insertSql += wxString::Format("?%zu", i + 1); + } + insertSql += ")"; + + if (!ExecuteSql(db, "BEGIN IMMEDIATE TRANSACTION", errorMessage)) { + sqlite3_close(db); + return false; + } + + sqlite3_stmt* statement = nullptr; + const wxCharBuffer utf8InsertSql = insertSql.utf8_str(); + const int prepareResult = sqlite3_prepare_v2(db, utf8InsertSql.data(), -1, &statement, nullptr); + if (prepareResult != SQLITE_OK) { + if (errorMessage) { + *errorMessage = wxString::Format("%s\n\nSQL:\n%s", SqliteError(db, "Unable to prepare SQLite export statement."), insertSql); + } + ExecuteSql(db, "ROLLBACK", nullptr); + sqlite3_close(db); + return false; + } + + wxProgressDialog progressDialog( + "Export To SQLite Database", + "Preparing export...", + std::max(1, static_cast(rows.size())), + parent, + wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME | wxPD_ESTIMATED_TIME | wxPD_REMAINING_TIME); + + for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) { + sqlite3_reset(statement); + sqlite3_clear_bindings(statement); + + const std::vector& row = rows[rowIndex]; + for (size_t col = 0; col < columnNames.size(); ++col) { + const wxString value = col < row.size() ? row[col] : wxString(); + if (!BindSqlValue(statement, static_cast(col + 1), columnTypes[col], value, errorMessage)) { + if (errorMessage && errorMessage->IsEmpty()) { + *errorMessage = wxString::Format("Unable to bind row %zu, column %zu while exporting.", rowIndex + 1, col + 1); + } + sqlite3_finalize(statement); + ExecuteSql(db, "ROLLBACK", nullptr); + sqlite3_close(db); + return false; + } + } + + const int stepResult = sqlite3_step(statement); + if (stepResult != SQLITE_DONE) { + if (errorMessage) { + *errorMessage = wxString::Format( + "SQLite export failed at row %zu.\n\n%s\n\nSQL:\n%s", + rowIndex + 1, + SqliteError(db, "Unable to insert row into SQLite table."), + insertSql); + } + sqlite3_finalize(statement); + ExecuteSql(db, "ROLLBACK", nullptr); + sqlite3_close(db); + return false; + } + + progressDialog.Update( + static_cast(rowIndex + 1), + wxString::Format("Exporting row %zu of %zu", rowIndex + 1, rows.size())); + } + + sqlite3_finalize(statement); + + if (!ExecuteSql(db, "COMMIT", errorMessage)) { + ExecuteSql(db, "ROLLBACK", nullptr); + sqlite3_close(db); + return false; + } + + sqlite3_close(db); + return true; +} + +} // namespace sqlite_dialog diff --git a/src/sqlite_dialog_common.h b/src/sqlite_dialog_common.h new file mode 100644 index 0000000..a8d176e --- /dev/null +++ b/src/sqlite_dialog_common.h @@ -0,0 +1,36 @@ +#pragma once + +#include "sqlite_import_dialog.h" + +#include + +#include + +class wxWindow; +class wxChoice; + +struct sqlite3; +struct sqlite3_stmt; + +namespace sqlite_dialog { + +wxString SqliteError(sqlite3* db, const wxString& fallback); +wxString QuoteSqlIdentifier(const wxString& identifier); +wxString BlobToHexString(const void* blob, int size); +bool OpenSqliteDatabase(const wxString& path, int flags, sqlite3** outDb, wxString* errorMessage); +bool LoadSqliteTableNames(const wxString& path, std::vector* tableNames, wxString* errorMessage); +bool ReadSqliteTable(const wxString& path, const wxString& tableName, ImportedSqliteTable* importedTable, wxString* errorMessage); +wxString SuggestedTableName(const wxString& documentName); +std::vector BuildExportColumnNames(const ImportedSqliteTable& table); +bool ExecuteSql(sqlite3* db, const wxString& sql, wxString* errorMessage); +bool BindSqlValue(sqlite3_stmt* statement, int index, const wxString& typeName, const wxString& value, wxString* errorMessage); +bool WriteSqliteTable( + wxWindow* parent, + const wxString& path, + const wxString& tableName, + const std::vector& columnNames, + const std::vector& columnTypes, + const std::vector>& rows, + wxString* errorMessage); + +} // namespace sqlite_dialog diff --git a/src/sqlite_export_dialog.cpp b/src/sqlite_export_dialog.cpp new file mode 100644 index 0000000..3feb173 --- /dev/null +++ b/src/sqlite_export_dialog.cpp @@ -0,0 +1,174 @@ +#include "sqlite_import_dialog.h" + +#include "sqlite_dialog_common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +class SqliteExportDialog final : public wxDialog { +public: + SqliteExportDialog(wxWindow* parent, const ImportedSqliteTable& table) + : wxDialog(parent, wxID_ANY, "Export To SQLite Database", wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_table(table), + m_columnNames(sqlite_dialog::BuildExportColumnNames(table)) { + auto* topSizer = new wxBoxSizer(wxVERTICAL); + + auto* databaseLabel = new wxStaticText(this, wxID_ANY, "SQLite database"); + topSizer->Add(databaseLabel, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); + + m_databasePicker = new wxFilePickerCtrl( + this, + wxID_ANY, + {}, + "Choose a SQLite database", + "SQLite databases (*.sqlite)|*.sqlite|All files (*.*)|*.*", + wxDefaultPosition, + wxDefaultSize, + wxFLP_SAVE); + topSizer->Add(m_databasePicker, 0, wxEXPAND | wxALL, FromDIP(12)); + + m_databasePathLabel = new wxStaticText(this, wxID_ANY, "No database selected"); + topSizer->Add(m_databasePathLabel, 0, wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); + + auto* tableLabel = new wxStaticText(this, wxID_ANY, "Table name"); + topSizer->Add(tableLabel, 0, wxLEFT | wxRIGHT, FromDIP(12)); + + m_tableNameCtrl = new wxTextCtrl(this, wxID_ANY, sqlite_dialog::SuggestedTableName(table.documentName)); + topSizer->Add(m_tableNameCtrl, 0, wxEXPAND | wxALL, FromDIP(12)); + + auto* mappingLabel = new wxStaticText(this, wxID_ANY, "Column types"); + topSizer->Add(mappingLabel, 0, wxLEFT | wxRIGHT, FromDIP(12)); + + auto* mappingPanel = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxBORDER_THEME); + mappingPanel->SetScrollRate(0, FromDIP(16)); + mappingPanel->SetMinSize(FromDIP(wxSize(-1, 260))); + mappingPanel->SetMaxSize(FromDIP(wxSize(-1, 260))); + + auto* mappingSizer = new wxFlexGridSizer(3, FromDIP(8), FromDIP(12)); + mappingSizer->AddGrowableCol(1, 1); + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "Column"), 0, wxALIGN_CENTER_VERTICAL); + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "Field name"), 0, wxALIGN_CENTER_VERTICAL); + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "SQLite type"), 0, wxALIGN_CENTER_VERTICAL); + + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "ID"), 0, wxALIGN_CENTER_VERTICAL); + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "ID"), 0, wxALIGN_CENTER_VERTICAL); + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "INTEGER PRIMARY KEY AUTOINCREMENT"), 0, wxALIGN_CENTER_VERTICAL); + + static const wxString typeChoices[] = { "TEXT", "INTEGER", "REAL", "NUMERIC", "BLOB" }; + for (size_t i = 0; i < m_columnNames.size(); ++i) { + const wxString originalLabel = i < m_table.headers.size() && !m_table.headers[i].IsEmpty() + ? m_table.headers[i] + : wxString::Format("Column %zu", i + 1); + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, originalLabel), 0, wxALIGN_CENTER_VERTICAL); + mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, m_columnNames[i]), 0, wxALIGN_CENTER_VERTICAL); + + auto* choice = new wxChoice(mappingPanel, wxID_ANY); + for (const wxString& typeChoice : typeChoices) { + choice->Append(typeChoice); + } + choice->SetStringSelection("TEXT"); + m_typeChoices.push_back(choice); + mappingSizer->Add(choice, 0, wxEXPAND); + } + + auto* mappingPanelSizer = new wxBoxSizer(wxVERTICAL); + mappingPanelSizer->Add(mappingSizer, 0, wxEXPAND | wxALL, FromDIP(12)); + mappingPanel->SetSizer(mappingPanelSizer); + mappingPanel->FitInside(); + topSizer->Add(mappingPanel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); + + m_statusLabel = new wxStaticText(this, wxID_ANY, wxString::Format("%zu rows will be exported.", m_table.rows.size())); + topSizer->Add(m_statusLabel, 0, wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); + + auto* buttonSizer = CreateSeparatedButtonSizer(wxOK | wxCANCEL); + topSizer->Add(buttonSizer, 0, wxEXPAND | wxALL, FromDIP(12)); + + SetSizerAndFit(topSizer); + SetMinSize(FromDIP(wxSize(640, 320))); + + if (wxButton* okButton = dynamic_cast(FindWindow(wxID_OK))) { + okButton->SetLabel("Export"); + okButton->Disable(); + } + + m_databasePicker->Bind(wxEVT_FILEPICKER_CHANGED, &SqliteExportDialog::OnDatabaseChanged, this); + m_tableNameCtrl->Bind(wxEVT_TEXT, &SqliteExportDialog::OnTableNameChanged, this); + Bind(wxEVT_BUTTON, &SqliteExportDialog::OnExport, this, wxID_OK); + UpdateExportButton(); + } + +private: + void UpdateExportButton() { + if (m_databasePathLabel) { + const wxString path = m_databasePicker->GetPath(); + m_databasePathLabel->SetLabel(path.IsEmpty() ? "No database selected" : path); + m_databasePathLabel->Wrap(FromDIP(600)); + Layout(); + } + + if (wxWindow* button = FindWindow(wxID_OK)) { + button->Enable(!m_databasePicker->GetPath().IsEmpty() && !m_tableNameCtrl->GetValue().Trim(true).Trim(false).IsEmpty()); + } + } + + void OnTableNameChanged(wxCommandEvent&) { + UpdateExportButton(); + } + + void OnDatabaseChanged(wxFileDirPickerEvent&) { + UpdateExportButton(); + } + + void OnExport(wxCommandEvent&) { + std::vector columnTypes; + columnTypes.reserve(m_typeChoices.size()); + for (wxChoice* choice : m_typeChoices) { + columnTypes.push_back(choice->GetStringSelection().IsEmpty() ? "TEXT" : choice->GetStringSelection()); + } + + const wxString databasePath = m_databasePicker->GetPath(); + const wxString tableName = m_tableNameCtrl->GetValue().Trim(true).Trim(false); + + wxString errorMessage; + if (!sqlite_dialog::WriteSqliteTable(this, databasePath, tableName, m_columnNames, columnTypes, m_table.rows, &errorMessage)) { + wxMessageBox( + wxString::Format( + "Unable to export to SQLite database.\n\nDatabase: %s\nTable: %s\n\n%s", + databasePath, + tableName, + errorMessage), + "Export To SQLite Database", + wxOK | wxICON_ERROR, + this); + return; + } + + EndModal(wxID_OK); + } + + const ImportedSqliteTable& m_table; + std::vector m_columnNames; + wxFilePickerCtrl* m_databasePicker{nullptr}; + wxStaticText* m_databasePathLabel{nullptr}; + wxTextCtrl* m_tableNameCtrl{nullptr}; + wxStaticText* m_statusLabel{nullptr}; + std::vector m_typeChoices; +}; + +} // namespace + +bool ShowSqliteExportDialog(wxWindow* parent, const ImportedSqliteTable& table) { + SqliteExportDialog dialog(parent, table); + return dialog.ShowModal() == wxID_OK; +} diff --git a/src/sqlite_import_dialog.cpp b/src/sqlite_import_dialog.cpp index e3a7f94..f98977e 100644 --- a/src/sqlite_import_dialog.cpp +++ b/src/sqlite_import_dialog.cpp @@ -1,415 +1,19 @@ #include "sqlite_import_dialog.h" -#include +#include "sqlite_dialog_common.h" #include #include #include #include -#include #include -#include -#include #include #include -#include -#include -#include -#include -#include #include namespace { -wxString SqliteError(sqlite3* db, const wxString& fallback) { - if (!db) { - return fallback; - } - - const char* message = sqlite3_errmsg(db); - if (!message || *message == '\0') { - return fallback; - } - - return wxString::FromUTF8(message); -} - -wxString QuoteSqlIdentifier(const wxString& identifier) { - wxString quoted = identifier; - quoted.Replace("\"", "\"\""); - return "\"" + quoted + "\""; -} - -wxString BlobToHexString(const void* blob, int size) { - if (!blob || size <= 0) { - return {}; - } - - const unsigned char* bytes = static_cast(blob); - wxString hex = "0x"; - static constexpr char digits[] = "0123456789ABCDEF"; - for (int i = 0; i < size; ++i) { - hex += digits[bytes[i] >> 4]; - hex += digits[bytes[i] & 0x0F]; - } - return hex; -} - -bool OpenSqliteDatabase(const wxString& path, int flags, sqlite3** outDb, wxString* errorMessage) { - *outDb = nullptr; - const wxCharBuffer utf8Path = path.utf8_str(); - const int result = sqlite3_open_v2(utf8Path.data(), outDb, flags, nullptr); - if (result == SQLITE_OK) { - return true; - } - - if (errorMessage) { - *errorMessage = SqliteError(*outDb, "Unable to open SQLite database."); - } - if (*outDb) { - sqlite3_close(*outDb); - *outDb = nullptr; - } - return false; -} - -bool LoadSqliteTableNames(const wxString& path, std::vector* tableNames, wxString* errorMessage) { - tableNames->clear(); - - sqlite3* db = nullptr; - if (!OpenSqliteDatabase(path, SQLITE_OPEN_READONLY, &db, errorMessage)) { - return false; - } - - static constexpr const char* query = - "SELECT name " - "FROM sqlite_master " - "WHERE type = 'table' AND name NOT LIKE 'sqlite_%' " - "ORDER BY name"; - - sqlite3_stmt* statement = nullptr; - const int prepareResult = sqlite3_prepare_v2(db, query, -1, &statement, nullptr); - if (prepareResult != SQLITE_OK) { - if (errorMessage) { - *errorMessage = SqliteError(db, "Unable to read table list from SQLite database."); - } - sqlite3_close(db); - return false; - } - - while (sqlite3_step(statement) == SQLITE_ROW) { - const unsigned char* text = sqlite3_column_text(statement, 0); - if (text) { - tableNames->push_back(wxString::FromUTF8(reinterpret_cast(text))); - } - } - - sqlite3_finalize(statement); - sqlite3_close(db); - return true; -} - -bool ReadSqliteTable(const wxString& path, const wxString& tableName, ImportedSqliteTable* importedTable, wxString* errorMessage) { - importedTable->documentName = tableName + ".csv"; - importedTable->headers.clear(); - importedTable->rows.clear(); - - sqlite3* db = nullptr; - if (!OpenSqliteDatabase(path, SQLITE_OPEN_READONLY, &db, errorMessage)) { - return false; - } - - const wxString query = "SELECT * FROM " + QuoteSqlIdentifier(tableName); - sqlite3_stmt* statement = nullptr; - const wxCharBuffer utf8Query = query.utf8_str(); - const int prepareResult = sqlite3_prepare_v2(db, utf8Query.data(), -1, &statement, nullptr); - if (prepareResult != SQLITE_OK) { - if (errorMessage) { - *errorMessage = SqliteError(db, "Unable to read selected SQLite table."); - } - sqlite3_close(db); - return false; - } - - const int columnCount = sqlite3_column_count(statement); - importedTable->headers.reserve(static_cast(columnCount)); - for (int col = 0; col < columnCount; ++col) { - const char* name = sqlite3_column_name(statement, col); - importedTable->headers.push_back(name ? wxString::FromUTF8(name) : wxString::Format("Column %d", col + 1)); - } - - int stepResult = SQLITE_ROW; - while ((stepResult = sqlite3_step(statement)) == SQLITE_ROW) { - std::vector row; - row.reserve(static_cast(columnCount)); - for (int col = 0; col < columnCount; ++col) { - switch (sqlite3_column_type(statement, col)) { - case SQLITE_NULL: - row.emplace_back(); - break; - case SQLITE_BLOB: - row.push_back(BlobToHexString(sqlite3_column_blob(statement, col), sqlite3_column_bytes(statement, col))); - break; - default: { - const unsigned char* value = sqlite3_column_text(statement, col); - row.push_back(value ? wxString::FromUTF8(reinterpret_cast(value)) : wxString()); - break; - } - } - } - importedTable->rows.push_back(std::move(row)); - } - - sqlite3_finalize(statement); - sqlite3_close(db); - - if (stepResult != SQLITE_DONE) { - if (errorMessage) { - *errorMessage = "Unable to finish reading the selected SQLite table."; - } - return false; - } - - return true; -} - -wxString SuggestedTableName(const wxString& documentName) { - wxString tableName = wxFileName(documentName).GetName(); - if (tableName.IsEmpty()) { - tableName = documentName; - } - if (tableName.IsEmpty()) { - tableName = "imported_data"; - } - - for (size_t i = 0; i < tableName.Length(); ++i) { - const wxUniChar ch = tableName[i]; - if (!(wxIsalnum(ch) || ch == '_')) { - tableName[i] = '_'; - } - } - - if (tableName.IsEmpty() || wxIsdigit(tableName[0])) { - tableName.Prepend("table_"); - } - return tableName; -} - -std::vector BuildExportColumnNames(const ImportedSqliteTable& table) { - const size_t columnCount = table.headers.size(); - std::vector names; - names.reserve(columnCount); - - for (size_t i = 0; i < columnCount; ++i) { - wxString name = table.headers[i]; - if (name.IsEmpty()) { - name = wxString::Format("Column_%zu", i + 1); - } - - for (size_t j = 0; j < name.Length(); ++j) { - const wxUniChar ch = name[j]; - if (!(wxIsalnum(ch) || ch == '_')) { - name[j] = '_'; - } - } - - if (name.IsEmpty() || wxIsdigit(name[0])) { - name.Prepend("Column_"); - } - - if (name.CmpNoCase("ID") == 0) { - name += "_value"; - } - - const wxString baseName = name; - int suffix = 2; - while (std::any_of(names.begin(), names.end(), [&name](const wxString& existing) { - return existing.CmpNoCase(name) == 0; - })) { - name = wxString::Format("%s_%d", baseName, suffix++); - } - - names.push_back(name); - } - - return names; -} - -bool ExecuteSql(sqlite3* db, const wxString& sql, wxString* errorMessage) { - char* rawError = nullptr; - const wxCharBuffer utf8Sql = sql.utf8_str(); - const int result = sqlite3_exec(db, utf8Sql.data(), nullptr, nullptr, &rawError); - if (result == SQLITE_OK) { - return true; - } - - if (errorMessage) { - wxString sqliteMessage = rawError ? wxString::FromUTF8(rawError) : SqliteError(db, "SQLite command failed."); - *errorMessage = wxString::Format("%s\n\nSQL:\n%s", sqliteMessage, sql); - } - if (rawError) { - sqlite3_free(rawError); - } - return false; -} - -bool BindSqlValue(sqlite3_stmt* statement, int index, const wxString& typeName, const wxString& value, wxString* errorMessage) { - if (value.IsEmpty()) { - return sqlite3_bind_null(statement, index) == SQLITE_OK; - } - - if (typeName == "INTEGER") { - long long integerValue = 0; - if (value.ToLongLong(&integerValue)) { - return sqlite3_bind_int64(statement, index, static_cast(integerValue)) == SQLITE_OK; - } - } else if (typeName == "REAL") { - double realValue = 0.0; - if (value.ToDouble(&realValue)) { - return sqlite3_bind_double(statement, index, realValue) == SQLITE_OK; - } - } else if (typeName == "NUMERIC") { - long long integerValue = 0; - if (value.ToLongLong(&integerValue)) { - return sqlite3_bind_int64(statement, index, static_cast(integerValue)) == SQLITE_OK; - } - double realValue = 0.0; - if (value.ToDouble(&realValue)) { - return sqlite3_bind_double(statement, index, realValue) == SQLITE_OK; - } - } else if (typeName == "BLOB") { - const wxCharBuffer utf8Value = value.utf8_str(); - return sqlite3_bind_blob(statement, index, utf8Value.data(), static_cast(strlen(utf8Value.data())), SQLITE_TRANSIENT) == SQLITE_OK; - } - - const wxCharBuffer utf8Value = value.utf8_str(); - const int result = sqlite3_bind_text(statement, index, utf8Value.data(), -1, SQLITE_TRANSIENT); - if (result != SQLITE_OK && errorMessage) { - *errorMessage = "Unable to bind a value for SQLite export."; - } - return result == SQLITE_OK; -} - -bool WriteSqliteTable( - wxWindow* parent, - const wxString& path, - const wxString& tableName, - const std::vector& columnNames, - const std::vector& columnTypes, - const std::vector>& rows, - wxString* errorMessage) { - sqlite3* db = nullptr; - if (!OpenSqliteDatabase(path, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &db, errorMessage)) { - return false; - } - - wxString createSql = "CREATE TABLE " + QuoteSqlIdentifier(tableName) + " (" - + QuoteSqlIdentifier("ID") + " INTEGER PRIMARY KEY AUTOINCREMENT"; - for (size_t i = 0; i < columnNames.size(); ++i) { - createSql += ", " + QuoteSqlIdentifier(columnNames[i]) + " " + columnTypes[i]; - } - createSql += ")"; - - if (!ExecuteSql(db, createSql, errorMessage)) { - sqlite3_close(db); - return false; - } - - wxString insertSql = "INSERT INTO " + QuoteSqlIdentifier(tableName); - if (!columnNames.empty()) { - insertSql += " ("; - for (size_t i = 0; i < columnNames.size(); ++i) { - if (i > 0) { - insertSql += ", "; - } - insertSql += QuoteSqlIdentifier(columnNames[i]); - } - insertSql += ")"; - } - insertSql += " VALUES ("; - for (size_t i = 0; i < columnNames.size(); ++i) { - if (i > 0) { - insertSql += ", "; - } - insertSql += wxString::Format("?%zu", i + 1); - } - insertSql += ")"; - - if (!ExecuteSql(db, "BEGIN IMMEDIATE TRANSACTION", errorMessage)) { - sqlite3_close(db); - return false; - } - - sqlite3_stmt* statement = nullptr; - const wxCharBuffer utf8InsertSql = insertSql.utf8_str(); - const int prepareResult = sqlite3_prepare_v2(db, utf8InsertSql.data(), -1, &statement, nullptr); - if (prepareResult != SQLITE_OK) { - if (errorMessage) { - *errorMessage = wxString::Format("%s\n\nSQL:\n%s", SqliteError(db, "Unable to prepare SQLite export statement."), insertSql); - } - ExecuteSql(db, "ROLLBACK", nullptr); - sqlite3_close(db); - return false; - } - - wxProgressDialog progressDialog( - "Export To SQLite Database", - "Preparing export...", - std::max(1, static_cast(rows.size())), - parent, - wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME | wxPD_ESTIMATED_TIME | wxPD_REMAINING_TIME); - - for (size_t rowIndex = 0; rowIndex < rows.size(); ++rowIndex) { - sqlite3_reset(statement); - sqlite3_clear_bindings(statement); - - const std::vector& row = rows[rowIndex]; - for (size_t col = 0; col < columnNames.size(); ++col) { - const wxString value = col < row.size() ? row[col] : wxString(); - if (!BindSqlValue(statement, static_cast(col + 1), columnTypes[col], value, errorMessage)) { - if (errorMessage && errorMessage->IsEmpty()) { - *errorMessage = wxString::Format("Unable to bind row %zu, column %zu while exporting.", rowIndex + 1, col + 1); - } - sqlite3_finalize(statement); - ExecuteSql(db, "ROLLBACK", nullptr); - sqlite3_close(db); - return false; - } - } - - const int stepResult = sqlite3_step(statement); - if (stepResult != SQLITE_DONE) { - if (errorMessage) { - *errorMessage = wxString::Format( - "SQLite export failed at row %zu.\n\n%s\n\nSQL:\n%s", - rowIndex + 1, - SqliteError(db, "Unable to insert row into SQLite table."), - insertSql); - } - sqlite3_finalize(statement); - ExecuteSql(db, "ROLLBACK", nullptr); - sqlite3_close(db); - return false; - } - - progressDialog.Update( - static_cast(rowIndex + 1), - wxString::Format("Exporting row %zu of %zu", rowIndex + 1, rows.size())); - } - - sqlite3_finalize(statement); - - if (!ExecuteSql(db, "COMMIT", errorMessage)) { - ExecuteSql(db, "ROLLBACK", nullptr); - sqlite3_close(db); - return false; - } - - sqlite3_close(db); - return true; -} - class SqliteImportDialog final : public wxDialog { public: explicit SqliteImportDialog(wxWindow* parent) @@ -483,7 +87,7 @@ class SqliteImportDialog final : public wxDialog { } wxString errorMessage; - if (!LoadSqliteTableNames(path, &m_tables, &errorMessage)) { + if (!sqlite_dialog::LoadSqliteTableNames(path, &m_tables, &errorMessage)) { m_statusLabel->SetLabel(errorMessage); UpdateImportButton(); Layout(); @@ -525,7 +129,7 @@ class SqliteImportDialog final : public wxDialog { } wxString errorMessage; - if (!ReadSqliteTable(m_databasePicker->GetPath(), m_tableChoice->GetString(selection), &m_importedTable, &errorMessage)) { + if (!sqlite_dialog::ReadSqliteTable(m_databasePicker->GetPath(), m_tableChoice->GetString(selection), &m_importedTable, &errorMessage)) { wxMessageBox(errorMessage, "Import SQLite Table", wxOK | wxICON_ERROR, this); return; } @@ -543,155 +147,6 @@ class SqliteImportDialog final : public wxDialog { bool m_hasImportedTable{false}; }; -class SqliteExportDialog final : public wxDialog { -public: - SqliteExportDialog(wxWindow* parent, const ImportedSqliteTable& table) - : wxDialog(parent, wxID_ANY, "Export To SQLite Database", wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), - m_table(table), - m_columnNames(BuildExportColumnNames(table)) { - auto* topSizer = new wxBoxSizer(wxVERTICAL); - - auto* databaseLabel = new wxStaticText(this, wxID_ANY, "SQLite database"); - topSizer->Add(databaseLabel, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(12)); - - m_databasePicker = new wxFilePickerCtrl( - this, - wxID_ANY, - {}, - "Choose a SQLite database", - "SQLite databases (*.sqlite)|*.sqlite|All files (*.*)|*.*", - wxDefaultPosition, - wxDefaultSize, - wxFLP_SAVE); - topSizer->Add(m_databasePicker, 0, wxEXPAND | wxALL, FromDIP(12)); - - m_databasePathLabel = new wxStaticText(this, wxID_ANY, "No database selected"); - topSizer->Add(m_databasePathLabel, 0, wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); - - auto* tableLabel = new wxStaticText(this, wxID_ANY, "Table name"); - topSizer->Add(tableLabel, 0, wxLEFT | wxRIGHT, FromDIP(12)); - - m_tableNameCtrl = new wxTextCtrl(this, wxID_ANY, SuggestedTableName(table.documentName)); - topSizer->Add(m_tableNameCtrl, 0, wxEXPAND | wxALL, FromDIP(12)); - - auto* mappingLabel = new wxStaticText(this, wxID_ANY, "Column types"); - topSizer->Add(mappingLabel, 0, wxLEFT | wxRIGHT, FromDIP(12)); - - auto* mappingPanel = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxBORDER_THEME); - mappingPanel->SetScrollRate(0, FromDIP(16)); - mappingPanel->SetMinSize(FromDIP(wxSize(-1, 260))); - mappingPanel->SetMaxSize(FromDIP(wxSize(-1, 260))); - - auto* mappingSizer = new wxFlexGridSizer(3, FromDIP(8), FromDIP(12)); - mappingSizer->AddGrowableCol(1, 1); - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "Column"), 0, wxALIGN_CENTER_VERTICAL); - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "Field name"), 0, wxALIGN_CENTER_VERTICAL); - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "SQLite type"), 0, wxALIGN_CENTER_VERTICAL); - - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "ID"), 0, wxALIGN_CENTER_VERTICAL); - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "ID"), 0, wxALIGN_CENTER_VERTICAL); - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, "INTEGER PRIMARY KEY AUTOINCREMENT"), 0, wxALIGN_CENTER_VERTICAL); - - static const wxString typeChoices[] = { "TEXT", "INTEGER", "REAL", "NUMERIC", "BLOB" }; - for (size_t i = 0; i < m_columnNames.size(); ++i) { - const wxString originalLabel = i < m_table.headers.size() && !m_table.headers[i].IsEmpty() - ? m_table.headers[i] - : wxString::Format("Column %zu", i + 1); - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, originalLabel), 0, wxALIGN_CENTER_VERTICAL); - mappingSizer->Add(new wxStaticText(mappingPanel, wxID_ANY, m_columnNames[i]), 0, wxALIGN_CENTER_VERTICAL); - - auto* choice = new wxChoice(mappingPanel, wxID_ANY); - for (const wxString& typeChoice : typeChoices) { - choice->Append(typeChoice); - } - choice->SetStringSelection("TEXT"); - m_typeChoices.push_back(choice); - mappingSizer->Add(choice, 0, wxEXPAND); - } - auto* mappingPanelSizer = new wxBoxSizer(wxVERTICAL); - mappingPanelSizer->Add(mappingSizer, 0, wxEXPAND | wxALL, FromDIP(12)); - mappingPanel->SetSizer(mappingPanelSizer); - mappingPanel->FitInside(); - topSizer->Add(mappingPanel, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); - - m_statusLabel = new wxStaticText(this, wxID_ANY, wxString::Format("%zu rows will be exported.", m_table.rows.size())); - topSizer->Add(m_statusLabel, 0, wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(12)); - - auto* buttonSizer = CreateSeparatedButtonSizer(wxOK | wxCANCEL); - topSizer->Add(buttonSizer, 0, wxEXPAND | wxALL, FromDIP(12)); - - SetSizerAndFit(topSizer); - SetMinSize(FromDIP(wxSize(640, 320))); - - if (wxButton* okButton = dynamic_cast(FindWindow(wxID_OK))) { - okButton->SetLabel("Export"); - okButton->Disable(); - } - - m_databasePicker->Bind(wxEVT_FILEPICKER_CHANGED, &SqliteExportDialog::OnDatabaseChanged, this); - m_tableNameCtrl->Bind(wxEVT_TEXT, &SqliteExportDialog::OnTableNameChanged, this); - Bind(wxEVT_BUTTON, &SqliteExportDialog::OnExport, this, wxID_OK); - UpdateExportButton(); - } - -private: - void UpdateExportButton() { - if (m_databasePathLabel) { - const wxString path = m_databasePicker->GetPath(); - m_databasePathLabel->SetLabel(path.IsEmpty() ? "No database selected" : path); - m_databasePathLabel->Wrap(FromDIP(600)); - Layout(); - } - - if (wxWindow* button = FindWindow(wxID_OK)) { - button->Enable(!m_databasePicker->GetPath().IsEmpty() && !m_tableNameCtrl->GetValue().Trim(true).Trim(false).IsEmpty()); - } - } - - void OnTableNameChanged(wxCommandEvent&) { - UpdateExportButton(); - } - - void OnDatabaseChanged(wxFileDirPickerEvent&) { - UpdateExportButton(); - } - - void OnExport(wxCommandEvent&) { - std::vector columnTypes; - columnTypes.reserve(m_typeChoices.size()); - for (wxChoice* choice : m_typeChoices) { - columnTypes.push_back(choice->GetStringSelection().IsEmpty() ? "TEXT" : choice->GetStringSelection()); - } - - const wxString databasePath = m_databasePicker->GetPath(); - const wxString tableName = m_tableNameCtrl->GetValue().Trim(true).Trim(false); - - wxString errorMessage; - if (!WriteSqliteTable(this, databasePath, tableName, m_columnNames, columnTypes, m_table.rows, &errorMessage)) { - wxMessageBox( - wxString::Format( - "Unable to export to SQLite database.\n\nDatabase: %s\nTable: %s\n\n%s", - databasePath, - tableName, - errorMessage), - "Export To SQLite Database", - wxOK | wxICON_ERROR, - this); - return; - } - - EndModal(wxID_OK); - } - - const ImportedSqliteTable& m_table; - std::vector m_columnNames; - wxFilePickerCtrl* m_databasePicker{nullptr}; - wxStaticText* m_databasePathLabel{nullptr}; - wxTextCtrl* m_tableNameCtrl{nullptr}; - wxStaticText* m_statusLabel{nullptr}; - std::vector m_typeChoices; -}; - } // namespace bool ShowSqliteImportDialog(wxWindow* parent, ImportedSqliteTable* importedTable) { @@ -702,8 +157,3 @@ bool ShowSqliteImportDialog(wxWindow* parent, ImportedSqliteTable* importedTable return dialog.GetImportedTable(importedTable); } - -bool ShowSqliteExportDialog(wxWindow* parent, const ImportedSqliteTable& table) { - SqliteExportDialog dialog(parent, table); - return dialog.ShowModal() == wxID_OK; -}