From b5ea0fc21a45a5139b06a7342e353245de6bcf9e Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 00:36:27 +0800 Subject: [PATCH 01/16] Refactor: Finish refactor --- .clang-format | 22 +- .vscode/launch.json | 9 + .zed/debug.json | 10 + .zed/tasks.json | 20 + Cmakelists.txt | 7 +- src/License.hpp | 22 + src/app.cpp | 296 ------ src/app.h | 89 -- src/canvas.h | 123 --- src/config.cpp | 8 - src/config.h | 15 - src/core/Document.cpp | 98 ++ src/core/Document.hpp | 57 ++ src/core/DocumentData.hpp | 94 ++ src/core/Event.hpp | 23 + src/core/ImageScaleModel.cpp | 94 ++ src/core/ImageScaleModel.hpp | 56 ++ src/core/Page.cpp | 162 +++ src/core/Page.hpp | 56 ++ src/ctrl.cpp | 126 --- src/prj.cpp | 300 ------ src/prj.h | 162 --- src/ui/App.cpp | 23 + src/ui/App.hpp | 15 + src/{canvas.cpp => ui/Canvas.cpp} | 943 ++++++++---------- src/ui/Canvas.hpp | 74 ++ src/ui/ConfigPanel.cpp | 58 ++ src/ui/ConfigPanel.hpp | 47 + src/ui/Defs.hpp | 13 + src/ui/MainFrame.cpp | 364 +++++++ src/ui/MainFrame.hpp | 78 ++ src/ui/MenuBar.cpp | 41 + src/ui/MenuBar.hpp | 15 + src/ui/ToolBar.cpp | 49 + src/ui/ToolBar.hpp | 12 + src/ui/components/SliderWithSpin.cpp | 90 ++ .../components/SliderWithSpin.hpp} | 82 +- src/utils/Asserts.hpp | 47 + src/utils/Compare.cpp | 29 + src/utils/Compare.hpp | 6 + src/wxUI.cpp | 140 --- src/wxUI.h | 65 -- 42 files changed, 2150 insertions(+), 1890 deletions(-) create mode 100644 .zed/debug.json create mode 100644 .zed/tasks.json create mode 100644 src/License.hpp delete mode 100644 src/app.cpp delete mode 100644 src/app.h delete mode 100644 src/canvas.h delete mode 100644 src/config.cpp delete mode 100644 src/config.h create mode 100644 src/core/Document.cpp create mode 100644 src/core/Document.hpp create mode 100644 src/core/DocumentData.hpp create mode 100644 src/core/Event.hpp create mode 100644 src/core/ImageScaleModel.cpp create mode 100644 src/core/ImageScaleModel.hpp create mode 100644 src/core/Page.cpp create mode 100644 src/core/Page.hpp delete mode 100644 src/ctrl.cpp delete mode 100644 src/prj.cpp delete mode 100644 src/prj.h create mode 100644 src/ui/App.cpp create mode 100644 src/ui/App.hpp rename src/{canvas.cpp => ui/Canvas.cpp} (58%) create mode 100644 src/ui/Canvas.hpp create mode 100644 src/ui/ConfigPanel.cpp create mode 100644 src/ui/ConfigPanel.hpp create mode 100644 src/ui/Defs.hpp create mode 100644 src/ui/MainFrame.cpp create mode 100644 src/ui/MainFrame.hpp create mode 100644 src/ui/MenuBar.cpp create mode 100644 src/ui/MenuBar.hpp create mode 100644 src/ui/ToolBar.cpp create mode 100644 src/ui/ToolBar.hpp create mode 100644 src/ui/components/SliderWithSpin.cpp rename src/{ctrl.h => ui/components/SliderWithSpin.hpp} (55%) create mode 100644 src/utils/Asserts.hpp create mode 100644 src/utils/Compare.cpp create mode 100644 src/utils/Compare.hpp delete mode 100644 src/wxUI.cpp delete mode 100644 src/wxUI.h diff --git a/.clang-format b/.clang-format index 5fc7c04..fc18271 100644 --- a/.clang-format +++ b/.clang-format @@ -1,22 +1,24 @@ --- -Language: Cpp -BasedOnStyle: Google +Language: Cpp +BasedOnStyle: Google IndentWidth: 4 ColumnLimit: 100 -#IncludeBlocks: Preserve # 保持头文件分组结构 -SortIncludes: CaseInsensitive # 开启排序(但通过分组控制例外) +SortIncludes: CaseInsensitive # 开启排序(但通过分组控制例外) +AlignConsecutiveDeclarations: true # 开启对连续声明的对齐。 +AlignConsecutiveAssignments: true +AlignTrailingComments: true IncludeCategories: # 1. 优先排序标准库头文件(尖括号) - - Regex: '^<[a-z_]+>' - Priority: 1 + - Regex: "^<[a-z_]+>" + Priority: 1 # 2. 禁止排序第三方库(通过低优先级 + 不排序同一分组) - - Regex: '^<*.*>' # 匹配第三方路径 - Priority: 2 + - Regex: "^<*.*>" # 匹配第三方路径 + Priority: 2 # 3. 其他头文件(如项目自有头文件) - - Regex: '^".*"' - Priority: 3 + - Regex: '^".*"' + Priority: 3 ... diff --git a/.vscode/launch.json b/.vscode/launch.json index 7ef1025..a7c2d7d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,15 @@ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Launch", + "program": "${workspaceFolder}/build/Croplines.exe", + "args": [], + "cwd": "${workspaceFolder}/build", + "preLaunchTask": "CMake: 生成", + }, { "name": "(gdb) 启动", "type": "cppdbg", diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..8b375ec --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,10 @@ +[ + { + "label": "Debug", + "cwd": "./build", + "program": "Croplines.exe", + "request": "launch", + "adapter": "GDB", + "build": "CMake build debug" + } +] diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 0000000..1f7322e --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,20 @@ +[ + { + "label": "CMake build debug", + "command": "cmake --build build --config Debug --target all -j 20 --", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "never", + "shell": "system", + }, + { + "label": "CMake build release", + "command": "cmake --build build -j 20 --config Release", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "never", + "shell": "system", + }, +] diff --git a/Cmakelists.txt b/Cmakelists.txt index 9e2d19d..2b49914 100644 --- a/Cmakelists.txt +++ b/Cmakelists.txt @@ -4,7 +4,7 @@ project(Croplines) add_compile_options("$<$:/source-charset:utf-8>") set(CMAKE_CXX_STANDARD 20) -set (CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static-libgcc") set(CMAKE_CXX_FLAGS_RELEASE "-s") @@ -28,10 +28,11 @@ set (WIN32_RESOURCES src/resource.rc) endif() if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) - add_executable(Croplines ${SRCS} ${WIN32_RESOURCES}) +add_executable(Croplines ${SRCS} ${WIN32_RESOURCES}) else() - add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) +add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) endif() +target_include_directories(Croplines PRIVATE src) target_link_libraries(Croplines ${wxWidgets_LIBRARIES}) target_link_libraries(Croplines ${OpenCV_LIBS}) target_link_libraries(Croplines opengl32) diff --git a/src/License.hpp b/src/License.hpp new file mode 100644 index 0000000..adc7400 --- /dev/null +++ b/src/License.hpp @@ -0,0 +1,22 @@ +[[maybe_unused]] static constexpr char LICENSE[]{ + "MIT License\n" + "\n" + "Copyright (c) 2024 Likend\n" + "\n" + "Permission is hereby granted, free of charge, to any person obtaining a copy\n" + "of this software and associated documentation files (the \"Software\"), to deal\n" + "in the Software without restriction, including without limitation the rights\n" + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" + "copies of the Software, and to permit persons to whom the Software is\n" + "furnished to do so, subject to the following conditions:\n" + "\n" + "The above copyright notice and this permission notice shall be included in all\n" + "copies or substantial portions of the Software.\n" + "\n" + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n" + "SOFTWARE.\n"}; diff --git a/src/app.cpp b/src/app.cpp deleted file mode 100644 index 262f099..0000000 --- a/src/app.cpp +++ /dev/null @@ -1,296 +0,0 @@ -#include "app.h" - -#include -#include - -#include -#include -#include -#include - -#include "ctrl.h" - -using namespace Croplines; - -// // 设置加速器表 -// const static wxAcceleratorEntry accel_entries[] = { -// {wxACCEL_CTRL, 'S', wxID_SAVE}, -// {wxACCEL_CTRL, 'O', wxID_OPEN}, -// {wxACCEL_CTRL, 'W', wxID_EXIT}, -// {wxACCEL_NORMAL, WXK_UP, wxID_UP}, -// {wxACCEL_NORMAL, WXK_DOWN, wxID_DOWN}}; -// const static wxAcceleratorTable accel_table(std::size(accel_entries), -// accel_entries); - -MainWindow::MainWindow(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, - const wxSize& size, long style) - : MainUI(parent, id, title, pos, size, style) { - SetIcon(wxICON(MAIN_ICON)); - - menubar = new MenuBar(); - SetMenuBar(menubar); - - EnableTools(false); - EnableMenu(false); - EnableConfigs(false); - - // SetAcceleratorTable(accel_table); -} - -void MainWindow::EnableTools(bool state) { - toolbar->EnableTool(wxID_UP, state); - toolbar->EnableTool(wxID_DOWN, state); - toolbar->EnableTool(wxID_SAVE, state); - // toolbar->EnableTool(wxID_OPEN, state); - toolbar->EnableTool(wxID_ZOOM_FIT, state); - toolbar->EnableTool(btnid_CROP_CURR_PAGE, state); - toolbar->EnableTool(btnid_CROP_ALL_PAGE, state); - toolbar->Refresh(); -} - -void MainWindow::EnableConfigs(bool enable) { - ntbk_cfg_output->Enable(enable); - ntbk_cfg_process->Enable(enable); - sld_cfg_border->Enable(enable); - sld_cfg_pix_filter->Enable(enable); -} - -void MainWindow::EnableMenu(bool enable) { - menubar->Enable(wxID_SAVE, enable); - menubar->Enable(wxID_CLOSE, enable); - menubar->Enable(wxID_UP, enable); - menubar->Enable(wxID_DOWN, enable); - menubar->Enable(wxID_ZOOM_FIT, enable); - menubar->Enable(wxID_ZOOM_OUT, enable); - menubar->Enable(wxID_ZOOM_IN, enable); - menubar->Enable(wxID_ZOOM_100, enable); - menubar->Enable(btnid_CROP_CURR_PAGE, enable); - menubar->Enable(btnid_CROP_ALL_PAGE, enable); - if (!enable) { - menubar->Enable(wxID_UNDO, false); - menubar->Enable(wxID_REDO, false); - } else { - menubar->Enable(wxID_UNDO, prj->CanUndo()); - menubar->Enable(wxID_REDO, prj->CanRedo()); - } -} - -void MainWindow::Load(std::filesystem::path path) { - auto prj_res = Prj::Load(path); - if (prj_res) { - prj = std::move(prj_res); - prj->menubar = menubar; - - // panel page list - std::vector file_names(prj->GetPages().size()); - std::ranges::transform(prj->GetPages(), file_names.begin(), [](const Prj::Page& page) { - return wxString(page.image_path.filename()); - }); - pn_page_list->Set(file_names); - if (!prj->GetPages().empty()) { - CurrentPage(0); - ShowPage(); - } - sld_cfg_border->SetValue(static_cast(prj->config.border)); - sld_cfg_pix_filter->SetValue(static_cast(prj->config.filter_noise_size)); - - EnableTools(true); - EnableConfigs(true); - EnableMenu(true); - canvas->SetPrj(*prj); - SetTitle(wxString(path.filename())); - } -} - -static bool ShowCloseDialog(wxWindow* parent, Prj& prj) { - wxMessageDialog dialog(parent, wxT("项目已更改,是否保存?"), - wxASCII_STR(wxMessageBoxCaptionStr), - wxICON_QUESTION | wxYES_NO | wxCANCEL); - switch (dialog.ShowModal()) { - case wxID_YES: - prj.Save(); - case wxID_NO: - return true; - - case wxID_CANCEL: - default: - return false; - } -} - -bool MainWindow::ClosePrj() { - if (prj) { - if (prj->IsChange()) { - if (!ShowCloseDialog(this, *prj)) return false; - } - EnableTools(false); - EnableMenu(false); - EnableConfigs(false); - prj->GetPages()[CurrentPage()].get().Close(); - prj.reset(); - canvas->Clear(); - pn_page_list->Clear(); - } - return true; -} - -bool MainWindow::Exit() { - if (ClosePrj()) { - Destroy(); - return true; - } else - return false; -} - -void MainWindow::CurrentPage(std::size_t page) { - if (prj) { - current_page = std::clamp(page, static_cast(0), prj->GetPages().size() - 1); - toolbar->EnableTool(wxID_UP, current_page != 0); - toolbar->EnableTool(wxID_DOWN, current_page != prj->GetPages().size() - 1); - pn_page_list->SetSelection(static_cast(current_page)); - } -} - -void MainWindow::PrevPage() { - if (prj && CurrentPage() != 0) { - prj->GetPages()[CurrentPage()].get().Close(); - CurrentPage(CurrentPage() - 1); - ShowPage(); - } -} - -void MainWindow::NextPage() { - if (prj && CurrentPage() < prj->GetPages().size()) { - prj->GetPages()[CurrentPage()].get().Close(); - CurrentPage(CurrentPage() + 1); - ShowPage(); - } -} - -void MainWindow::ShowPage() { - canvas->SetPage(prj->GetPages()[CurrentPage()]); - canvas->Refresh(); -} - -void MainWindow::OnLoad([[maybe_unused]] wxCommandEvent& event) { - if (prj && prj->IsChange()) { - if (!ShowCloseDialog(this, *prj)) return; - } - wxDirDialog dir_dialog(this, wxT("选择目录"), wxEmptyString, - wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); - auto result = dir_dialog.ShowModal(); - if (result == wxID_OK) { - wxString dir = dir_dialog.GetPath(); - Load(std::filesystem::path(dir.utf8_string())); - } -} - -void MainWindow::OnUndo([[maybe_unused]] wxCommandEvent& event) { - if (prj) { - prj->Undo(); - canvas->Refresh(); - menubar->Enable(wxID_UNDO, prj->CanUndo()); - menubar->Enable(wxID_REDO, true); - } -} - -void MainWindow::OnRedo([[maybe_unused]] wxCommandEvent& event) { - if (prj) { - prj->Redo(); - canvas->Refresh(); - menubar->Enable(wxID_UNDO, true); - menubar->Enable(wxID_REDO, prj->CanRedo()); - } -} - -void MainWindow::OnCropCurrPage([[maybe_unused]] wxCommandEvent& event) { - if (canvas->IsLoaded()) { - Prj::Page& page = *canvas->page; - SetStatusText(std::format("Page {} is croping...", CurrentPage() + 1)); - if (prj->SaveCrops(page)) { - SetStatusText(std::format("Page {} finished!", CurrentPage() + 1)); - } else { - SetStatusText(std::format("Page {} failed!", CurrentPage() + 1)); - } - } -} - -void MainWindow::OnCropAllPage([[maybe_unused]] wxCommandEvent& event) { - if (canvas->IsLoaded()) { - std::thread t([this]() { - std::size_t count = 1; - for (Prj::Page& page : prj->GetPages()) { - SetStatusText(std::format("Page {} is croping...", count)); - prj->SaveCrops(page); - count++; - } - SetStatusText(wxT("Croping finised!")); - }); - t.detach(); - } -} - -void MainWindow::OnAbout([[maybe_unused]] wxCommandEvent& event) { - static const char LICENSE[] = { -#embed "../LICENSE" - }; - - wxAboutDialogInfo aboutInfo; - aboutInfo.SetName(wxT("Croplines")); - aboutInfo.SetIcon(wxICON(MAIN_ICON)); - // aboutInfo.AddArtist(wxT("Likend")); - aboutInfo.AddDeveloper(wxT("Likend")); - aboutInfo.SetWebSite(wxT("https://github.com/Likend/Croplines")); - aboutInfo.SetCopyright(wxT("(C) 2024-2025")); - aboutInfo.SetLicence(LICENSE); - wxAboutBox(aboutInfo); -} - -void MainWindow::OnClickListBox(wxCommandEvent& event) { - if (CurrentPage() != event.GetSelection()) { - CurrentPage(event.GetSelection()); - ShowPage(); - } -} - -// clang-format off -wxBEGIN_EVENT_TABLE(MainWindow, wxFrame) - EVT_MENU(wxID_UP, MainWindow::OnPrevPage) - EVT_MENU(wxID_DOWN, MainWindow::OnNextPage) - EVT_MENU(wxID_SAVE, MainWindow::OnSave) - EVT_MENU(wxID_OPEN, MainWindow::OnLoad) - EVT_MENU(wxID_UNDO, MainWindow::OnUndo) - EVT_MENU(wxID_REDO, MainWindow::OnRedo) - EVT_MENU(wxID_ZOOM_IN, MainWindow::OnZoomIn) - EVT_MENU(wxID_ZOOM_OUT, MainWindow::OnZoomOut) - EVT_MENU(wxID_ZOOM_FIT, MainWindow::OnZoomFit) - EVT_MENU(wxID_ZOOM_100, MainWindow::OnZoom100) - EVT_MENU(btnid_CROP_CURR_PAGE, MainWindow::OnCropCurrPage) - EVT_MENU(btnid_CROP_ALL_PAGE, MainWindow::OnCropAllPage) - EVT_MENU(wxID_CLOSE, MainWindow::OnClose) - EVT_MENU(wxID_EXIT, MainWindow::OnExit) - EVT_MENU(wxID_ABOUT, MainWindow::OnAbout) - EVT_CLOSE(MainWindow::OnExit) - EVT_LISTBOX(pnid_PAGE_LIST, MainWindow::OnClickListBox) - EVT_SLIDER(sldid_cfg_PIX_FILTER, MainWindow::OnChnageCfgFilerPixSize) - EVT_SLIDER(sldid_cfg_BORDER, MainWindow::OnChangeCfgBorder) -wxEND_EVENT_TABLE(); -// clang-format on - -// 告诉wxWidgets主应用程序是哪个类 -IMPLEMENT_APP(MyApp) - -// 初始化程序 -bool MyApp::OnInit() { - SetAppearance(Appearance::System); - // wxImage::AddHandler(new wxBMPHandler); - wxImage::AddHandler(new wxPNGHandler); - wxImage::AddHandler(new wxTIFFHandler); - wxImage::AddHandler(new wxJPEGHandler); - wxImage::AddHandler(new wxWEBPHandler); - - frame = new MainWindow(nullptr); // 创建主窗口 - frame->Show(true); // 显示主窗口 - - return true; // 开始事件处理循环 -} diff --git a/src/app.h b/src/app.h deleted file mode 100644 index 2c07103..0000000 --- a/src/app.h +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "wxUI.h" - -namespace Croplines { - -class MainWindow final : public MainUI { - public: - MainWindow(wxWindow* parent, wxWindowID id = wxID_ANY, - const wxString& title = wxT("Crop Lines"), const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxSize(972, 651), - long style = wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL); - ~MainWindow() {}; - - void EnableTools(bool state); - void EnableConfigs(bool enable); - void EnableMenu(bool enable); - - void Load(std::filesystem::path path); - void Save() { - if (prj) prj->Save(); - } - bool ClosePrj(); - bool Exit(); // true if close succesfully; false if cancelled - - std::size_t CurrentPage() { return current_page; } - void CurrentPage(std::size_t); - void PrevPage(); - void NextPage(); - - private: - std::size_t current_page = 0; - wxMenuBar* menubar; - - private: - void ShowPage(); - - void OnPrevPage([[maybe_unused]] wxCommandEvent& event) { PrevPage(); } - void OnNextPage([[maybe_unused]] wxCommandEvent& event) { NextPage(); } - void OnSave([[maybe_unused]] wxCommandEvent& event) { Save(); } - void OnLoad(wxCommandEvent& event); - void OnUndo(wxCommandEvent& event); - void OnRedo(wxCommandEvent& event); - void OnZoomIn([[maybe_unused]] wxCommandEvent& event) { canvas->ZoomIn(); } - void OnZoomOut([[maybe_unused]] wxCommandEvent& event) { canvas->ZoomOut(); } - void OnZoomFit([[maybe_unused]] wxCommandEvent& event) { canvas->ZoomFit(); } - void OnZoom100([[maybe_unused]] wxCommandEvent& event) { canvas->Zoom(1.0); } - void OnCropCurrPage(wxCommandEvent& event); - void OnCropAllPage(wxCommandEvent& event); - void OnClose([[maybe_unused]] wxCommandEvent& event) { ClosePrj(); } - void OnExit(wxCloseEvent& event) { - if (!Exit()) event.Veto(); - } - void OnExit([[maybe_unused]] wxCommandEvent& event) { Close(); } - void OnAbout(wxCommandEvent& event); - void OnClickListBox(wxCommandEvent& event); - void OnChnageCfgFilerPixSize(wxCommandEvent& event) { - prj->config.filter_noise_size = event.GetInt(); - prj->Change(); - } - void OnChangeCfgBorder(wxCommandEvent& event) { - prj->config.border = event.GetInt(); - prj->Change(); - } - - wxDECLARE_EVENT_TABLE(); - - private: - std::optional prj; -}; - -class MyApp : public wxApp { - private: - MainWindow* frame; - - public: - // 这个函数将会在程序启动的时候被调用 - virtual bool OnInit(); -}; - -// 有了这一行就可以使用 MyApp& wxGetApp()了 -DECLARE_APP(MyApp) -} // namespace Croplines diff --git a/src/canvas.h b/src/canvas.h deleted file mode 100644 index 690f9cf..0000000 --- a/src/canvas.h +++ /dev/null @@ -1,123 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "prj.h" - -namespace Croplines { - -class ImageScaleModel { - public: - wxSize imageSize; - wxSize windowSize; - wxSize scaledSize; - // matrix trans - // [[scale, 0, offset_x, - // 0, scale, offset_y ]] - double scale; - wxPoint offset; - - ImageScaleModel() = default; - ImageScaleModel(wxSize imageSize, wxSize windowSize); - ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale); - - private: - void Clamp(); - - public: - void Scale(double factor, wxPoint center); - void Scale(double factor) { Scale(factor, wxPoint{} + windowSize / 2); } - void ScaleTo(double scale, wxPoint center); - void ScaleTo(double scale) { ScaleTo(scale, wxPoint{} + windowSize / 2); } - void Move(wxPoint dr); - void MoveToCenter(); - void WindowResize(wxSize windowSizeNew); - void ImageResize(wxSize imageSizeNew); - - double GetScaleSuitesPage() const; - double GetScaleSuitesWidth() const; - double GetScaleSuitesHeight() const; - - cv::Mat GetTransformMatrix() const; - - wxRealPoint Transform(wxRealPoint point) const { return scale * point + wxRealPoint(offset); } - double TransformX(double x) const { return scale * x + offset.x; } - double TransformY(double y) const { return scale * y + offset.y; } - wxRealPoint ReverseTransform(wxRealPoint point) const { - return (point - wxRealPoint(offset)) / scale; - } - double ReverseTransformX(double x) const { return (x - offset.x) / scale; } - double ReverseTransformY(double y) const { return (y - offset.y) / scale; } - - bool IsInsideImage(wxRealPoint worldPoint) const; - - private: - friend class Canvas; - bool modified = true; -}; - -class Canvas : public wxGLCanvas { - public: - Prj* prj = nullptr; - Prj::Page* page = nullptr; - ImageScaleModel scaleModel; - - private: - wxGLContext* context = nullptr; - GLuint texture; - cv::UMat uimageSrc; - wxImage imageDst; - bool imageModified = false; - - public: - Canvas(wxWindow* parent, wxWindowID id); - ~Canvas(); - void SetPrj(Prj& prj) { this->prj = &prj; } - void SetPage(Prj::Page& pageData); - void Clear(); - bool IsLoaded() const { return page; } - ImageScaleModel& GetScaleModel() { return scaleModel; } - - void ZoomIn(); - void ZoomOut(); - void ZoomFit(); - void Zoom(double scale); - - private: - bool is_deleting = false; - bool is_mouse_capture = false; - std::optional mouse_drag_start; - std::optional mouse_position; - bool initialized = false; - wxBitmap drawBmp; - - private: - void UpdateScrollbars(); - - void OnPaint(wxPaintEvent& event); - void OnSize(wxSizeEvent& event); - - void OnMouseWheel(wxMouseEvent& event); - void OnMouseLeftDown(wxMouseEvent& event); - void OnMouseLeftUp(wxMouseEvent& event); - void OnMouseLeftUp(wxMouseCaptureLostEvent& event); - void OnMouseRightDown(wxMouseEvent& event); - void OnMouseRightUp(wxMouseEvent& event); - void OnMouseMotion(wxMouseEvent& event); - - void OnScroll(wxScrollWinEvent& event); - - void OnKeyDown(wxKeyEvent& event); - void OnKeyUp(wxKeyEvent& event); - - void OnKillFocus(wxFocusEvent& event); - - wxDECLARE_EVENT_TABLE(); -}; -} // namespace Croplines diff --git a/src/config.cpp b/src/config.cpp deleted file mode 100644 index c1eaac5..0000000 --- a/src/config.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "config.h" - -using namespace Croplines; - -double Config::zoom_in_rate = ZOOM_IN_RATE_DEFAULT; -double Config::zoom_out_rate = ZOOM_OUT_RATE_DEFAULT; -double Config::zoom_min = ZOOM_MIN_DEFAULT; -double Config::zoom_max = ZOOM_MAX_DEFAULT; \ No newline at end of file diff --git a/src/config.h b/src/config.h deleted file mode 100644 index ba7f68d..0000000 --- a/src/config.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -namespace Croplines { -namespace Config { -constexpr double ZOOM_IN_RATE_DEFAULT = 1.2; -constexpr double ZOOM_OUT_RATE_DEFAULT = 1 / ZOOM_IN_RATE_DEFAULT; -constexpr double ZOOM_MIN_DEFAULT = 0.1; -constexpr double ZOOM_MAX_DEFAULT = 100; - -extern double zoom_in_rate; -extern double zoom_out_rate; -extern double zoom_min; -extern double zoom_max; -} // namespace Config -} // namespace Croplines diff --git a/src/core/Document.cpp b/src/core/Document.cpp new file mode 100644 index 0000000..2507a5a --- /dev/null +++ b/src/core/Document.cpp @@ -0,0 +1,98 @@ +#include "core/Document.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "core/DocumentData.hpp" +#include "utils/Asserts.hpp" +#include "utils/Compare.hpp" + +using namespace Croplines; +namespace fs = std::filesystem; + +bool Document::Load(const fs::path& path) { + cwd = path; + + if (!fs::exists(path)) return false; + fs::current_path(path); + + m_data.emplace(); + + fs::path prj_path = path / PROJECT_FILE_NAME; + if (fs::exists(prj_path) && fs::is_regular_file(prj_path)) { + std::ifstream file(prj_path); + if (file) { + cereal::JSONInputArchive archive(file); + archive(cereal::make_nvp("prj", getData())); + m_modified = false; + return true; + } + } + InitializeEmptyProject(); + return true; +} + +void Document::InitializeEmptyProject() { + auto extensionFilter = [](const fs::directory_entry& entry) { + return entry.is_regular_file() && + std::ranges::find(VALID_EXTENSION, entry.path().extension().string()) != + std::end(VALID_EXTENSION); + }; + for (auto const& entry : fs::directory_iterator(cwd) | std::views::filter(extensionFilter)) { + fs::path relativePath = fs::relative(entry.path(), cwd); + m_data->pages.push_back(std::make_unique(relativePath)); + } + + // sort pages + auto pred = [](const std::unique_ptr& pageData) { + return pageData->path.filename().string(); + }; + std::ranges::sort( + m_data->pages, + [](std::string_view s1, std::string_view s2) { return NaturalCompare(s1, s2) < 0; }, pred); + + setModified(); +} + +bool Document::Save() { + ASSERT_WITH(isLoad(), "Project not loaded!"); + fs::path prj_path = cwd / PROJECT_FILE_NAME; + + std::ofstream file(prj_path); + cereal::JSONOutputArchive archive(file); + archive(cereal::make_nvp("prj", getData())); + m_modified = false; + return true; +} + +bool Document::Close() { + ASSERT_WITH(isLoad(), "Project not loaded!"); + m_modified = false; + m_data.reset(); + m_processor->ClearCommands(); + return true; +} + +void Document::setModified(bool modified) { m_modified = modified; } + +Page Document::LoadPage(size_t index) { + ASSERT_WITH(index < PagesSize(), "indx out of range"); + auto& pageData = *m_data->pages[index]; + return Page{*this, pageData}; +} + +bool Document::SaveAllCrops() { + ASSERT_WITH(isLoad(), "Project not loaded!"); + bool success = true; + for (size_t i = 0; i < PagesSize(); i++) { + Page page = LoadPage(i); + success &= page.SaveCrops(); + } + return success; +} diff --git a/src/core/Document.hpp b/src/core/Document.hpp new file mode 100644 index 0000000..73e79a5 --- /dev/null +++ b/src/core/Document.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "core/DocumentData.hpp" +#include "core/Event.hpp" +#include "core/Page.hpp" + +namespace Croplines { + +class Document { + public: + constexpr static const char* VALID_EXTENSION[] = {".png", ".jpg", ".jpeg", ".bmp", + ".tiff", ".tif", ".webp"}; + constexpr static const char* PROJECT_FILE_NAME = "croplines.json"; + + bool Load(const std::filesystem::path& path); + bool Save(); + bool Close(); + bool isLoad() const { return m_data.has_value(); }; + + const std::filesystem::path& GetPath() const { return cwd; } + + wxCommandProcessor* GetProcessor() const { return m_processor; } + + bool isModified() const { return m_modified; } + void setModified(bool modified = true); + + size_t PagesSize() const { return m_data->pages.size(); }; + Page LoadPage(size_t index); + + bool SaveAllCrops(); + + DocumentData& getData() { return m_data.value(); } + DocumentConfig& getConfig() { return getData().config; } + + private: + bool m_modified = false; + + std::optional m_data; + + std::filesystem::path cwd; + + wxCommandProcessor* m_processor = new wxCommandProcessor(); + + void InitializeEmptyProject(); +}; + +} // namespace Croplines diff --git a/src/core/DocumentData.hpp b/src/core/DocumentData.hpp new file mode 100644 index 0000000..2c4fdf0 --- /dev/null +++ b/src/core/DocumentData.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Croplines { +struct DocumentConfig { + constexpr static const char* DEFAULT_OUTPUT_DIR = "out"; + + std::filesystem::path output_dir = DEFAULT_OUTPUT_DIR; + int border = 10; + int filter_noise_size = 8; + + template + void serialize(Archive& archive) { + archive(cereal::make_nvp("border", border)); + archive(cereal::make_nvp("filter_noise_size", filter_noise_size)); + archive(cereal::make_nvp("output_dir", output_dir)); + } +}; + +struct PageData { + std::filesystem::path path; + std::set crop_lines; + + PageData() = default; + PageData(std::filesystem::path path) : path(std::move(path)) {} + + template + void serialize(Archive& archive) { + archive(cereal::make_nvp("image_path", path)); + archive(cereal::make_nvp("crop_lines", crop_lines)); + } +}; + +struct DocumentData { + DocumentConfig config; + + std::vector> pages; + + struct PagesProxy { + std::vector>& pages; + + template + void save(Archive& ar) const { + ar(cereal::make_size_tag(static_cast(pages.size()))); + for (auto const& p : pages) { + if (p) ar(*p); + } + } + + template + void load(Archive& ar) { + cereal::size_type size; + ar(cereal::make_size_tag(size)); + pages.clear(); + pages.reserve(size); + for (size_t i = 0; i < size; ++i) { + auto p = std::make_unique(); + ar(*p); + pages.push_back(std::move(p)); + } + } + }; + + template + void serialize(Archive& ar) { + ar(cereal::make_nvp("config", config)); + ar(cereal::make_nvp("pages", PagesProxy{pages})); + } +}; +} // namespace Croplines + +// Add support for cereal serializing std::filesystem::path +namespace std { +namespace filesystem { +template +inline void load_minimal(const Archive&, std::filesystem::path& path, const std::string& name) { + path = name; +} + +template +inline std::string save_minimal(const Archive&, const std::filesystem::path& path) { + return path.string(); +} +} // namespace filesystem +} // namespace std diff --git a/src/core/Event.hpp b/src/core/Event.hpp new file mode 100644 index 0000000..50ef5d2 --- /dev/null +++ b/src/core/Event.hpp @@ -0,0 +1,23 @@ +#include +#include + +namespace Croplines { + +wxDECLARE_EVENT(EVT_DOCUMENT_CHANGED, wxCommandEvent); + +class DocumentEvent final : public wxCommandEvent { + public: + enum Type { Loaded, Saved, UndoDone, RedoDone, Modified }; + + DocumentEvent(Type actionType) : wxCommandEvent(EVT_DOCUMENT_CHANGED), m_type(actionType) {} + DocumentEvent(const DocumentEvent& other) = default; + + DocumentEvent* Clone() const override { return new DocumentEvent(*this); } + + Type GetActionType() const { return m_type; } + + private: + Type m_type; +}; + +} // namespace Croplines diff --git a/src/core/ImageScaleModel.cpp b/src/core/ImageScaleModel.cpp new file mode 100644 index 0000000..1c71a13 --- /dev/null +++ b/src/core/ImageScaleModel.cpp @@ -0,0 +1,94 @@ +#include "core/ImageScaleModel.hpp" + +using namespace Croplines; + +ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale) + : imageSize(imageSize), windowSize(windowSize), scale(scale) { + scaledSize = imageSize * scale; + MoveToCenter(); +} + +ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize) + : imageSize(imageSize), windowSize(windowSize) { + scale = GetScaleSuitesPage(); + scaledSize = imageSize * scale; + MoveToCenter(); +} + +void ImageScaleModel::Clamp() { + wxSize border = windowSize - scaledSize; + if (border.x < 0) { + offset.x = std::clamp(offset.x, border.x, 0); + } else { + offset.x = std::clamp(offset.x, 0, border.x); + } + if (border.y < 0) { + offset.y = std::clamp(offset.y, border.y, 0); + } else { + offset.y = std::clamp(offset.y, 0, border.y); + } +} + +void ImageScaleModel::Scale(double factor, wxPoint center) { + double scaleNew = scale * factor; + if (scaleNew > ZOOM_MAX) { + factor = ZOOM_MAX / scale; + } else if (scaleNew < ZOOM_MIN) { + factor = ZOOM_MIN / scale; + } + scale *= factor; + offset = center + factor * (offset - center); + scaledSize = imageSize * scale; + Clamp(); +} + +void ImageScaleModel::ScaleTo(double scale, wxPoint center) { + scale = std::clamp(scale, ZOOM_MIN, ZOOM_MAX); + double factor = scale / this->scale; + this->scale = scale; + offset = center + factor * (offset - center); + scaledSize = imageSize * scale; + Clamp(); +} + +void ImageScaleModel::Move(wxPoint dr) { + offset += dr; + Clamp(); +} + +void ImageScaleModel::MoveToCenter() { offset = wxPoint{} + (windowSize - scaledSize) / 2; } + +void ImageScaleModel::OnWindowResize(wxSize windowSizeNew) { + offset += (windowSizeNew - windowSize) / 2; + windowSize = windowSizeNew; + Clamp(); +} + +void ImageScaleModel::OnImageResize(wxSize imageSizeNew) { + offset += (imageSize - imageSizeNew) / 2; + imageSize = imageSizeNew; + scaledSize = scale * imageSize; + Clamp(); +} + +double ImageScaleModel::GetScaleSuitesWidth() const { + return static_cast(windowSize.GetWidth()) / imageSize.GetWidth(); +} + +double ImageScaleModel::GetScaleSuitesHeight() const { + return static_cast(windowSize.GetHeight()) / imageSize.GetHeight(); +} + +double ImageScaleModel::GetScaleSuitesPage() const { + return std::min(GetScaleSuitesWidth(), GetScaleSuitesHeight()); +} + +cv::Mat ImageScaleModel::GetTransformMatrix() const { + return (cv::Mat_(2, 3) << scale, 0, offset.x, 0, scale, offset.y); +} + +bool ImageScaleModel::IsInsideImage(wxRealPoint worldPoint) const { + wxRealPoint imagePoint = ReverseTransform(worldPoint); + return 0 <= imagePoint.x && imagePoint.x <= imageSize.GetWidth() && 0 <= imagePoint.y && + imagePoint.y <= imageSize.GetHeight(); +} diff --git a/src/core/ImageScaleModel.hpp b/src/core/ImageScaleModel.hpp new file mode 100644 index 0000000..d4cf531 --- /dev/null +++ b/src/core/ImageScaleModel.hpp @@ -0,0 +1,56 @@ +#pragma once +#include +#include + +namespace Croplines { + +class ImageScaleModel { + public: + constexpr static double ZOOM_IN_RATE = 1.2; + constexpr static double ZOOM_OUT_RATE = 1 / ZOOM_IN_RATE; + constexpr static double ZOOM_MIN = 0.1; + constexpr static double ZOOM_MAX = 100; + + wxSize imageSize; + wxSize windowSize; + wxSize scaledSize; + // matrix trans + // [[scale, 0, offset_x, + // 0, scale, offset_y ]] + double scale; + wxPoint offset; + + ImageScaleModel() = default; + ImageScaleModel(wxSize imageSize, wxSize windowSize); + ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale); + + void Scale(double factor, wxPoint center); + void Scale(double factor) { Scale(factor, wxPoint{} + windowSize / 2); } + void ScaleTo(double scale, wxPoint center); + void ScaleTo(double scale) { ScaleTo(scale, wxPoint{} + windowSize / 2); } + void Move(wxPoint dr); + void MoveToCenter(); + void OnWindowResize(wxSize windowSizeNew); + void OnImageResize(wxSize imageSizeNew); + + double GetScaleSuitesPage() const; + double GetScaleSuitesWidth() const; + double GetScaleSuitesHeight() const; + + cv::Mat GetTransformMatrix() const; + + wxRealPoint Transform(wxRealPoint point) const { return scale * point + wxRealPoint(offset); } + double TransformX(double x) const { return scale * x + offset.x; } + double TransformY(double y) const { return scale * y + offset.y; } + wxRealPoint ReverseTransform(wxRealPoint point) const { + return (point - wxRealPoint(offset)) / scale; + } + double ReverseTransformX(double x) const { return (x - offset.x) / scale; } + double ReverseTransformY(double y) const { return (y - offset.y) / scale; } + + bool IsInsideImage(wxRealPoint worldPoint) const; + + private: + void Clamp(); +}; +} // namespace Croplines diff --git a/src/core/Page.cpp b/src/core/Page.cpp new file mode 100644 index 0000000..df5110a --- /dev/null +++ b/src/core/Page.cpp @@ -0,0 +1,162 @@ +#include "core/Page.hpp" + +#include +#include + +#include + +#include "core/Document.hpp" +#include "core/DocumentData.hpp" +#include "utils/Asserts.hpp" + +using namespace Croplines; +namespace fs = std::filesystem; + +DocumentConfig& Page::getConfig() const { return m_doc.getConfig(); } + +bool Page::InsertLine(int line) { + auto [it, modified] = m_pageData.crop_lines.insert(line); + m_modified |= modified; + getDocument().setModified(); + return modified; +} + +bool Page::EraseLine(int line) { + bool modified = m_pageData.crop_lines.erase(line) != 0; + m_modified |= modified; + getDocument().setModified(); + return modified; +} + +bool Page::SaveCrops() { + if (!m_image.IsOk()) return false; + + std::size_t count = 1; + fs::create_directories(getConfig().output_dir); + for (wxRect area : getSelectAreas()) { + wxImage sub_image = m_image.GetSubImage(area); + // TODO + // wxSize border_size = wxSize{static_cast(config.border), + // static_cast(config.border)}; + // wxBitmap bitmap(area.GetSize() + 2 * border_size); + // wxMemoryDC memDC; + // memDC.SelectObject(bitmap); + // memDC.SetBrush(*wxWHITE_BRUSH); + // memDC.DrawRectangle(wxPoint{}, bitmap.GetSize()); + // memDC.DrawBitmap(wxBitmap(sub_image), wxPoint{} + border_size); + + // optimize compress for tiff + int sample_per_pixel = m_image.GetOptionInt(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL); + int bits_per_sample = m_image.GetOptionInt(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE); + sub_image.SetOption(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL, sample_per_pixel); + sub_image.SetOption(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE, bits_per_sample); + if (sample_per_pixel == 1 && bits_per_sample == 1) + sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, 4); + else + sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, + m_image.GetOptionInt(wxIMAGE_OPTION_TIFF_COMPRESSION)); + + fs::path file_path = + getConfig().output_dir / std::format("{}-{}{}", getImagePath().stem().string(), count, + getImagePath().extension().string()); + // bitmap.SaveFile(wxString(file_path), image.GetType()); + sub_image.SaveFile(wxString(file_path), m_image.GetType()); + count++; + } + return true; +} + +std::optional Page::SearchNearestLine(int searchPosition, int threshold) const { + const std::set& cropLines = getCropLines(); + if (cropLines.empty()) return std::nullopt; + + auto it1 = cropLines.lower_bound(searchPosition); + if (it1 == cropLines.begin()) { + int d = *it1 - searchPosition; + if (d < threshold) return *it1; + } else if (it1 == cropLines.end()) { + --it1; + int d = searchPosition - *it1; + if (d < threshold) return *it1; + } else { + int d1 = *it1 - searchPosition; + auto it2 = it1; + --it2; + int d2 = searchPosition - *it2; + if (d1 < d2 && d1 < threshold) return *it1; + if (d2 < d1 && d2 < threshold) return *it2; + } + return std::nullopt; +} + +/*自动选择黑色像素区域 + filter_noise_size: 忽略黑像素的大小 + expand_size: 留边空白大小 +*/ +static std::optional CalculateSelectArea(cv::Mat image, int filter_noise_size, + int expand_size, int base_line) { + cv::Mat img_dst; + cv::cvtColor(image, img_dst, cv::COLOR_RGB2GRAY); + cv::threshold(img_dst, img_dst, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); + std::vector> contours; + cv::findContours(img_dst, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); + int x_min = INT_MAX, x_max = INT_MIN, y_min = INT_MAX, y_max = INT_MIN; + bool has_point = false; + for (const auto& contour : contours) { + if (cv::contourArea(contour) >= filter_noise_size) { + int x_min_inner = x_min, x_max_inner = x_max, y_min_inner = y_min, y_max_inner = y_max; + for (const auto& point : contour) { + if (point.x == 0 || point.x == image.cols - 1 || point.y == 0 || + point.y == image.rows - 1) { + goto skip_contour; + } + if (point.x < x_min_inner) x_min_inner = point.x; + if (point.x > x_max_inner) x_max_inner = point.x; + if (point.y < y_min_inner) y_min_inner = point.y; + if (point.y > y_max_inner) y_max_inner = point.y; + } + x_min = x_min_inner; + x_max = x_max_inner; + y_min = y_min_inner; + y_max = y_max_inner; + has_point = true; + skip_contour: + (void)0; + } + } + if (!has_point) return std::nullopt; + + int l = x_min - expand_size; + int t = y_min - expand_size + base_line; + int r = x_max + expand_size; + int b = y_max + expand_size + base_line; + if (l < 0) l = 0; + if (t < 0) t = 0; + if (r > image.cols) r = image.cols; + if (b > image.rows + base_line) b = image.rows + base_line; + return wxRect{wxPoint{l, t}, wxPoint{r, b}}; +} + +void Page::CalculateSelectAreas() { + m_selectAreas.clear(); + ASSERT_WITH(m_image.IsOk(), "Image not load!"); + cv::Mat image(m_image.GetHeight(), m_image.GetWidth(), CV_8UC3, + static_cast(m_image.GetData())); + + int prev_line = 0; + + auto invokeCalculation = [this, &image](int line, int prev_line) { + cv::Mat sub_image = image.rowRange(prev_line, line); + auto area = CalculateSelectArea(sub_image, getConfig().filter_noise_size, 0, prev_line); + if (area) m_selectAreas.push_back(*area); + }; + + for (int line : getCropLines()) { + invokeCalculation(line, prev_line); + prev_line = line; + } + std::int32_t line = image.rows; + invokeCalculation(line, prev_line); + + m_modified = false; +} diff --git a/src/core/Page.hpp b/src/core/Page.hpp new file mode 100644 index 0000000..270ac14 --- /dev/null +++ b/src/core/Page.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "core/DocumentData.hpp" + +namespace Croplines { + +class Document; + +class Page { + public: + ~Page() { m_image.Destroy(); } + + Document& getDocument() const { return m_doc; } + DocumentConfig& getConfig() const; + const std::filesystem::path& getImagePath() const { return m_pageData.path; } + const std::set& getCropLines() const { return m_pageData.crop_lines; } + wxImage& getImage() { return m_image; } + + const std::vector& getSelectAreas() { + if (m_modified) CalculateSelectAreas(); + return m_selectAreas; + } + + bool InsertLine(int line); + bool EraseLine(int line); + + bool SaveCrops(); + + std::optional SearchNearestLine(int searchPosition, int threshold) const; + + private: + friend class Document; + + Page(Document& doc, PageData& data) : m_doc(doc), m_pageData(data) { LoadImage(); } + + bool m_modified = true; // 用来提示 CalculateSelectAreas 的惰性求值 + + Document& m_doc; + PageData& m_pageData; + + std::vector m_selectAreas; + wxImage m_image; + + void LoadImage() { m_image.LoadFile(wxString(getImagePath())); } + void CalculateSelectAreas(); +}; +} // namespace Croplines diff --git a/src/ctrl.cpp b/src/ctrl.cpp deleted file mode 100644 index 21dd25d..0000000 --- a/src/ctrl.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include "ctrl.h" - -#include "wxUI.h" - -using namespace Croplines; - -constexpr int SLIDER_ID = 1100; -constexpr int SPIN_ID = 1101; - -SliderWithSpin::SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, - int minValue, int maxValue, const wxPoint& pos, const wxSize& size) - : wxPanel(parent, id, pos, size) { - m_label = new wxStaticText(this, wxID_ANY, label); - m_slider = new wxSlider(this, SLIDER_ID, value, minValue, maxValue); - m_spin = new wxSpinCtrl(this, SPIN_ID, wxEmptyString, wxDefaultPosition, wxDefaultSize, - wxSP_ARROW_KEYS, minValue, maxValue, value); - - wxBoxSizer* bSizer = new wxBoxSizer(wxHORIZONTAL); - bSizer->Add(m_label, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - bSizer->Add(m_slider, 1, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); - bSizer->Add(m_spin, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 5); - - SetSizer(bSizer); -} - -bool SliderWithSpin::Enable(bool enable) { - wxWindow* const items[] = {m_label, m_slider, m_spin}; - const bool prevEnable = IsEnabled(); - bool ret = wxPanel::Enable(enable); - if (!ret) { - return false; - } - for (const auto& item : items) { - ret = item->Enable(enable); - if (!ret) { - wxPanel::Enable(prevEnable); - for (const auto* it = items; it != &item; it++) { - (*it)->Enable(prevEnable); - } - return false; - } - } - return true; -} - -void SliderWithSpin::SetValue(int value) { - this->value = value; - if (m_spin->GetValue() != value) { - m_spin->SetValue(value); - } - if (m_slider->GetValue() != value) { - m_slider->SetValue(GetValue()); - } -} - -void SliderWithSpin::CallEvent(int value) { - wxCommandEvent evt(wxEVT_COMMAND_SLIDER_UPDATED, GetId()); - evt.SetEventObject(this); - evt.SetInt(value); - ProcessWindowEvent(evt); -} - -void SliderWithSpin::OnSliderChanged([[maybe_unused]] wxCommandEvent& event) { - const int value = m_slider->GetValue(); - if (m_spin->GetValue() != value) { - m_spin->SetValue(value); - } - this->value = value; - CallEvent(value); -} - -void SliderWithSpin::OnSpinChanged([[maybe_unused]] wxSpinEvent& event) { - const int value = m_spin->GetValue(); - if (m_slider->GetValue() != value) { - m_slider->SetValue(value); - } - this->value = value; - CallEvent(value); -} - -// clang-format off -wxBEGIN_EVENT_TABLE(SliderWithSpin, wxPanel) - EVT_SLIDER(SLIDER_ID, SliderWithSpin::OnSliderChanged) - EVT_SPINCTRL(SPIN_ID, SliderWithSpin::OnSpinChanged) -wxEND_EVENT_TABLE(); -// clang-format on - -MenuBar::MenuBar() : wxMenuBar() { - menu_file = new wxMenu(); - menu_file->Append(wxID_OPEN, wxT("&Load\tCtrl+O")); - menu_file->Append(wxID_SAVE); - menu_file->AppendSeparator(); - menu_file->Append(btnid_CROP_CURR_PAGE, wxT("Crop ¤t page"), - wxT("Crop current page to subimages and save each one to " - "output directory")); - menu_file->Append(btnid_CROP_ALL_PAGE, wxT("Crop &all pages"), - wxT("Crop all pages to subimages and save each one to " - "output directory")); - menu_file->AppendSeparator(); - menu_file->Append(wxID_CLOSE); - menu_file->Append(wxID_EXIT); - - Append(menu_file, wxT("&File")); - - menu_edit = new wxMenu(); - menu_edit->Append(wxID_UNDO); - menu_edit->Append(wxID_REDO); - menu_edit->AppendSeparator(); - menu_edit->Append(wxID_UP, wxT("Last page\tUp"), wxT("Move to last page")); - menu_edit->Append(wxID_DOWN, wxT("Next page\tDown"), wxT("Move to next page")); - - Append(menu_edit, wxT("&Edit")); - - menu_view = new wxMenu(); - menu_view->Append(wxID_ZOOM_IN); - menu_view->Append(wxID_ZOOM_OUT); - menu_view->Append(wxID_ZOOM_FIT); - menu_view->Append(wxID_ZOOM_100); - - Append(menu_view, wxT("&View")); - - menu_help = new wxMenu(); - menu_help->Append(wxID_ABOUT); - - Append(menu_help, wxT("&Help")); -} diff --git a/src/prj.cpp b/src/prj.cpp deleted file mode 100644 index 416879c..0000000 --- a/src/prj.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "prj.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -using namespace Croplines; - -namespace fs = std::filesystem; - -using Page = Prj::Page; - -wxImage Prj::LoadPage(Page& page) { - if (page.IsLoaded()) return page.image; - page.image.LoadFile(wxString(page.image_path)); - return page.image; -} - -bool Prj::SaveCrops(Page& page) { - // if (!page.IsLoaded()) LoadPage(page); - wxImage image = LoadPage(page); - std::size_t count = 1; - fs::create_directories(config.output_dir); - for (wxRect area : GetSelectArea(page)) { - wxImage sub_image = image.GetSubImage(area); - // TODO - // wxSize border_size = wxSize{static_cast(config.border), - // static_cast(config.border)}; - // wxBitmap bitmap(area.GetSize() + 2 * border_size); - // wxMemoryDC memDC; - // memDC.SelectObject(bitmap); - // memDC.SetBrush(*wxWHITE_BRUSH); - // memDC.DrawRectangle(wxPoint{}, bitmap.GetSize()); - // memDC.DrawBitmap(wxBitmap(sub_image), wxPoint{} + border_size); - - // optimize compress for tiff - int sample_per_pixel = image.GetOptionInt(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL); - int bits_per_sample = image.GetOptionInt(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE); - sub_image.SetOption(wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL, sample_per_pixel); - sub_image.SetOption(wxIMAGE_OPTION_TIFF_BITSPERSAMPLE, bits_per_sample); - if (sample_per_pixel == 1 && bits_per_sample == 1) - sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, 4); - else - sub_image.SetOption(wxIMAGE_OPTION_TIFF_COMPRESSION, - image.GetOptionInt(wxIMAGE_OPTION_TIFF_COMPRESSION)); - - fs::path file_path = - config.output_dir / std::format("{}-{}{}", page.image_path.stem().string(), count, - page.image_path.extension().string()); - // bitmap.SaveFile(wxString(file_path), image.GetType()); - sub_image.SaveFile(wxString(file_path), image.GetType()); - count++; - } - return true; -} - -/*自动选择黑色像素区域 - filter_noise_size: 忽略黑像素的大小 - expand_size: 留边空白大小 -*/ -static std::optional CalcuateSelectArea(cv::Mat image, int filter_noise_size, - int expand_size, int base_line) { - cv::Mat img_dst; - cv::cvtColor(image, img_dst, cv::COLOR_RGB2GRAY); - cv::threshold(img_dst, img_dst, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); - std::vector> contours; - cv::findContours(img_dst, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); - int x_min = INT_MAX, x_max = INT_MIN, y_min = INT_MAX, y_max = INT_MIN; - bool has_point = false; - for (const auto& contour : contours) { - if (cv::contourArea(contour) >= filter_noise_size) { - int x_min_inner = x_min, x_max_inner = x_max, y_min_inner = y_min, y_max_inner = y_max; - for (const auto& point : contour) { - if (point.x == 0 || point.x == image.cols - 1 || point.y == 0 || - point.y == image.rows - 1) { - goto skip_contour; - } - if (point.x < x_min_inner) x_min_inner = point.x; - if (point.x > x_max_inner) x_max_inner = point.x; - if (point.y < y_min_inner) y_min_inner = point.y; - if (point.y > y_max_inner) y_max_inner = point.y; - } - x_min = x_min_inner; - x_max = x_max_inner; - y_min = y_min_inner; - y_max = y_max_inner; - has_point = true; - skip_contour: - (void)0; - } - } - if (!has_point) return std::nullopt; - - int l = x_min - expand_size; - int t = y_min - expand_size + base_line; - int r = x_max + expand_size; - int b = y_max + expand_size + base_line; - if (l < 0) l = 0; - if (t < 0) t = 0; - if (r > image.cols) r = image.cols; - if (b > image.rows + base_line) b = image.rows + base_line; - return wxRect{wxPoint{l, t}, wxPoint{r, b}}; -} - -const std::vector& Prj::GetSelectArea(Page& page) { - if (page.modified) { - page.modified = false; - page.select_area.clear(); - wxImage ii = LoadPage(page); - cv::Mat image(ii.GetHeight(), ii.GetWidth(), CV_8UC3, static_cast(ii.GetData())); - int prev_line = 0; - for (int line : page.crop_lines) { - cv::Mat sub_image = image.rowRange(prev_line, line); - auto area = CalcuateSelectArea(sub_image, config.filter_noise_size, 0, prev_line); - if (area) page.select_area.push_back(*area); - prev_line = line; - } - std::int32_t line = image.rows; - cv::Mat sub_image = image.rowRange(prev_line, line); - auto area = CalcuateSelectArea(sub_image, config.filter_noise_size, 0, prev_line); - if (area) page.select_area.push_back(*area); - } - return page.select_area; -} - -std::optional::iterator> Page::SearchNearestLine(int key, int limit) const { - if (crop_lines.empty()) return std::nullopt; - - auto it1 = std::lower_bound(crop_lines.begin(), crop_lines.end(), key); - if (it1 == crop_lines.begin()) { - int d = *it1 - key; - if (d < limit) return it1; - } - if (it1 == crop_lines.end()) { - --it1; - int d = key - *it1; - if (d < limit) return it1; - } - int d1 = *it1 - key; - auto it2 = it1; - --it2; - int d2 = key - *it2; - if (d1 < d2 && d1 < limit) return it1; - if (d2 < d1 && d2 < limit) return it2; - - return std::nullopt; -} - -bool Prj::InsertLineRecord::Do([[maybe_unused]] Prj& prj) { - auto [_, actual_modified] = page.get().crop_lines.insert(line); - page.get().modified = true; - return actual_modified; -} - -void Prj::InsertLineRecord::Undo([[maybe_unused]] Prj& prj) { - page.get().crop_lines.erase(line); - page.get().modified = true; -} - -bool Prj::EraseLineRecord::Do([[maybe_unused]] Prj& prj) { - page.get().crop_lines.erase(it); - page.get().modified = true; - return true; -} - -void Prj::EraseLineRecord::Undo([[maybe_unused]] Prj& prj) { - page.get().crop_lines.insert(line); - page.get().modified = true; -} - -struct UndoVisitor { - std::reference_wrapper prj; - - template - void operator()(A& action) { - action.Undo(prj); - } -}; - -struct RedoVisitor { - std::reference_wrapper prj; - - template - void operator()(A& action) { - action.Do(prj); - } -}; - -void Prj::Undo() { - if (!undo_stack.empty()) { - auto action = undo_stack.top(); - undo_stack.pop(); - std::visit(UndoVisitor(*this), action); - redo_stack.push(action); - } -} - -void Prj::Redo() { - if (!redo_stack.empty()) { - auto action = redo_stack.top(); - redo_stack.pop(); - std::visit(RedoVisitor(*this), action); - undo_stack.push(action); - } -} - -static std::strong_ordering NaturalCompare(std::string_view a, std::string_view b) { - for (const char *i1 = a.begin(), *i2 = b.begin(); i1 != a.end() && i2 != b.end(); ++i1, ++i2) { - if (std::isdigit(*i1) && std::isdigit(*i2)) { - const char *ii1 = i1, *ii2 = i2; - while (ii1 != a.end() && *ii1 == '0') ++ii1; - while (ii2 != b.end() && *ii2 == '0') ++ii2; - auto zero_count1 = std::distance(i1, ii1); - auto zero_count2 = std::distance(i2, ii2); - - i1 = ii1; - i2 = ii2; - while (ii1 != a.end() && std::isdigit(*ii1)) ++ii1; - while (ii2 != b.end() && std::isdigit(*ii2)) ++ii2; - auto num1 = std::string_view(i1, ii1 - i1); - auto num2 = std::string_view(i2, ii2 - i2); - - if (auto cmp = num1.length() <=> num2.length(); cmp != 0) return cmp; - if (auto cmp = num1 <=> num2; cmp != 0) return cmp; - if (auto cmp = zero_count1 <=> zero_count2; cmp != 0) return cmp; - } else { - return std::toupper(*i1) <=> std::toupper(*i2); - } - } - return std::strong_ordering::equal; -} - -static std::vector> SortPages(std::vector& pages) { - std::vector> pages_sorted; - pages_sorted.reserve(pages.size()); - for (Page& page : pages) { - pages_sorted.push_back(std::ref(page)); - } - // natural sort - std::ranges::sort( - pages_sorted, - [](std::string_view s1, std::string_view s2) { return NaturalCompare(s1, s2) < 0; }, - [](const Page& page) { return page.image_path.filename().string(); }); - return pages_sorted; -} - -void Prj::Initialize() { - // Initialize project data with default values - config.output_dir = DEFAULT_OUTPUT_DIR; - config.border = 10; - config.filter_noise_size = 8; - - auto extension_filter = [](const fs::directory_entry& entry) { - return entry.is_regular_file() && - std::find(std::begin(VALID_EXTENSION), std::end(VALID_EXTENSION), - entry.path().extension().string()) != std::end(VALID_EXTENSION); - }; - for (auto const& dir_entry : - fs::directory_iterator(cwd) | std::views::filter(extension_filter)) { - Page page(fs::relative(dir_entry.path(), cwd)); - pages.push_back(page); - } - is_change = true; -} - -std::optional Prj::Load(const fs::path& path) { - if (!fs::exists(path)) return std::nullopt; - fs::current_path(path); - Prj prj; - prj.cwd = path; - fs::path prj_path = path / PROJECT_FILE_NAME; - if (fs::exists(prj_path) && fs::is_regular_file(prj_path)) { - std::ifstream file(prj_path); - if (file) { - cereal::JSONInputArchive archive(file); - archive(cereal::make_nvp("prj", prj)); - prj.is_change = false; - } else { - prj.Initialize(); - } - } else { - prj.Initialize(); - } - prj.pages_sorted = SortPages(prj.pages); - return prj; -} - -void Prj::Save() { - fs::path prj_path = cwd / PROJECT_FILE_NAME; - std::ofstream file(prj_path); - cereal::JSONOutputArchive archive(file); - archive(cereal::make_nvp("prj", *this)); - is_change = false; -} diff --git a/src/prj.h b/src/prj.h deleted file mode 100644 index 788ef58..0000000 --- a/src/prj.h +++ /dev/null @@ -1,162 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace Croplines { -constexpr const char* VALID_EXTENSION[] = {".png", ".jpg", ".jpeg", ".bmp", - ".tiff", ".tif", ".webp"}; -constexpr const char* PROJECT_FILE_NAME = "croplines.json"; -constexpr const char* DEFAULT_OUTPUT_DIR = "out"; - -class Prj; - -template -concept ActionRecord = requires(T record, Prj& prj) { - { record.Do(prj) } -> std::same_as; - { record.Undo(prj) } -> std::same_as; -}; - -class Prj { - public: - class Page { - friend class Prj; - std::set crop_lines; // 从小到大排序 - std::vector select_area; - bool modified = true; - wxImage image; // can be empty - public: - std::filesystem::path image_path; - - Page(std::filesystem::path image_path) : image_path(std::move(image_path)) {} - Page() = default; - - bool IsLoaded() const { return image.IsOk(); }; - void Close() { image.Destroy(); } - - const std::set GetCropLines() const { return crop_lines; } - std::optional::iterator> SearchNearestLine(int key, int limit) const; - - template - void serialize(Archive& archive) { - archive(cereal::make_nvp("image_path", image_path)); - archive(CEREAL_NVP(crop_lines)); - } - }; - - struct Config { - std::filesystem::path output_dir; - int border; - int filter_noise_size; - - template - void serialize(Archive& archive) { - archive(CEREAL_NVP(border)); - archive(CEREAL_NVP(filter_noise_size)); - archive(cereal::make_nvp("output_dir", output_dir)); - } - } config; - - class InsertLineRecord { - int line; - std::reference_wrapper page; - - public: - InsertLineRecord(int line, Page& page) : line(line), page(page) {} - bool Do(Prj& prj); - void Undo(Prj& prj); - }; - - class EraseLineRecord { - using SetIteratorType = decltype(Page::crop_lines)::iterator; - SetIteratorType it; - std::reference_wrapper page; - int line; - - public: - EraseLineRecord(SetIteratorType it, Page& page) : it(it), page(page), line(*it) {} - bool Do(Prj& prj); - void Undo(Prj& prj); - }; - - using Action = std::variant; - - private: - std::vector pages; - std::vector> pages_sorted; - std::filesystem::path cwd; // current working directory - bool is_change; - - std::stack undo_stack; - std::stack redo_stack; - - public: - wxMenuBar* menubar = nullptr; - - public: - static std::optional Load(const std::filesystem::path& path); - void Save(); - std::vector> GetPages() const { return pages_sorted; } - inline bool IsChange() { return is_change; } - inline void Change() { is_change = true; } - - wxImage LoadPage(Page& page); - bool SaveCrops(Page& page); - const std::vector& GetSelectArea(Page& page); - - template - void Execute(A action) { - bool success = action.Do(*this); - if (success) { - is_change = true; - undo_stack.push(action); - redo_stack = std::stack(); - if (menubar) { - menubar->Enable(wxID_UNDO, true); - menubar->Enable(wxID_REDO, false); - } - } - } - bool CanUndo() const { return !undo_stack.empty(); } - bool CanRedo() const { return !redo_stack.empty(); } - void Undo(); - void Redo(); - - template - void serialize(Archive& archive) { - archive(CEREAL_NVP(config)); - archive(CEREAL_NVP(pages)); - } - - private: - void Initialize(); -}; - -} // namespace Croplines - -// Add support for cereal serializing std::filesystem::path -namespace std { -namespace filesystem { -template -inline void load_minimal(const Archive&, std::filesystem::path& path, const std::string& name) { - path = name; -} - -template -inline std::string save_minimal(const Archive&, const std::filesystem::path& path) { - return path.string(); -} -} // namespace filesystem -} // namespace std diff --git a/src/ui/App.cpp b/src/ui/App.cpp new file mode 100644 index 0000000..b2b67e7 --- /dev/null +++ b/src/ui/App.cpp @@ -0,0 +1,23 @@ +#include "ui/App.hpp" + +#include +#include + +#include "ui/MainFrame.hpp" + +using namespace Croplines; + +IMPLEMENT_APP(CroplinesApp) + +bool CroplinesApp::OnInit() { + SetAppearance(Appearance::System); + wxImage::AddHandler(new wxPNGHandler); + wxImage::AddHandler(new wxTIFFHandler); + wxImage::AddHandler(new wxJPEGHandler); + wxImage::AddHandler(new wxWEBPHandler); + + frame = new MainFrame(nullptr, wxID_ANY, wxT("Croplines"), wxDefaultPosition, {900, 600}); + frame->Show(true); + + return true; +} diff --git a/src/ui/App.hpp b/src/ui/App.hpp new file mode 100644 index 0000000..fb8af99 --- /dev/null +++ b/src/ui/App.hpp @@ -0,0 +1,15 @@ +#pragma once +#include + +#include "ui/MainFrame.hpp" + +namespace Croplines { +class CroplinesApp : public wxApp { + private: + MainFrame* frame; + + public: + // 这个函数将会在程序启动的时候被调用 + bool OnInit() override; +}; +} // namespace Croplines diff --git a/src/canvas.cpp b/src/ui/Canvas.cpp similarity index 58% rename from src/canvas.cpp rename to src/ui/Canvas.cpp index bc2cee5..426d9b1 100644 --- a/src/canvas.cpp +++ b/src/ui/Canvas.cpp @@ -1,508 +1,435 @@ -#include "canvas.h" - -#include -#include -#include - -#include -#include - -#include "config.h" - -using namespace Croplines; - -ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale) - : imageSize(imageSize), windowSize(windowSize), scale(scale) { - scaledSize = imageSize * scale; - MoveToCenter(); -} - -ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize) - : imageSize(imageSize), windowSize(windowSize) { - scale = GetScaleSuitesPage(); - scaledSize = imageSize * scale; - MoveToCenter(); -} - -void ImageScaleModel::Clamp() { - wxSize border = windowSize - scaledSize; - if (border.x < 0) { - offset.x = std::clamp(offset.x, border.x, 0); - } else { - offset.x = std::clamp(offset.x, 0, border.x); - } - if (border.y < 0) { - offset.y = std::clamp(offset.y, border.y, 0); - } else { - offset.y = std::clamp(offset.y, 0, border.y); - } -} - -void ImageScaleModel::Scale(double factor, wxPoint center) { - double scaleNew = scale * factor; - if (scaleNew > Config::zoom_max) { - factor = Config::zoom_max / scale; - } else if (scaleNew < Config::zoom_min) { - factor = Config::zoom_min / scale; - } - scale *= factor; - offset = center + factor * (offset - center); - scaledSize = imageSize * scale; - Clamp(); - modified = true; -} - -void ImageScaleModel::ScaleTo(double scale, wxPoint center) { - scale = std::clamp(scale, Config::zoom_min, Config::zoom_max); - double factor = scale / this->scale; - this->scale = scale; - offset = center + factor * (offset - center); - scaledSize = imageSize * scale; - Clamp(); - modified = true; -} - -void ImageScaleModel::Move(wxPoint dr) { - offset += dr; - Clamp(); - modified = true; -} - -void ImageScaleModel::MoveToCenter() { - offset = wxPoint{} + (windowSize - scaledSize) / 2; - modified = true; -} - -void ImageScaleModel::WindowResize(wxSize windowSizeNew) { - offset += (windowSizeNew - windowSize) / 2; - windowSize = windowSizeNew; - Clamp(); - modified = true; -} - -void ImageScaleModel::ImageResize(wxSize imageSizeNew) { - offset += (imageSize - imageSizeNew) / 2; - imageSize = imageSizeNew; - scaledSize = scale * imageSize; - Clamp(); - modified = true; -} - -double ImageScaleModel::GetScaleSuitesWidth() const { - return static_cast(windowSize.GetWidth()) / imageSize.GetWidth(); -} - -double ImageScaleModel::GetScaleSuitesHeight() const { - return static_cast(windowSize.GetHeight()) / imageSize.GetHeight(); -} - -double ImageScaleModel::GetScaleSuitesPage() const { - return std::min(GetScaleSuitesWidth(), GetScaleSuitesHeight()); -} - -cv::Mat ImageScaleModel::GetTransformMatrix() const { - return (cv::Mat_(2, 3) << scale, 0, offset.x, 0, scale, offset.y); -} - -bool ImageScaleModel::IsInsideImage(wxRealPoint worldPoint) const { - wxRealPoint imagePoint = ReverseTransform(worldPoint); - return 0 <= imagePoint.x && imagePoint.x <= imageSize.GetWidth() && 0 <= imagePoint.y && - imagePoint.y <= imageSize.GetHeight(); -} - -Canvas::Canvas(wxWindow* parent, wxWindowID id) - : wxGLCanvas(parent, id, nullptr, wxDefaultPosition, wxDefaultSize, - wxFULL_REPAINT_ON_RESIZE | wxVSCROLL | wxHSCROLL) { - AlwaysShowScrollbars(); - - // // disable on default - SetScrollbar(wxHORIZONTAL, -1, -1, -1); - SetScrollbar(wxVERTICAL, -1, -1, -1); - - context = new wxGLContext(this); -} - -Canvas::~Canvas() { - if (texture) glDeleteTextures(1, &texture); - delete context; -} - -static void SetTextrue(GLuint texture, void* pixels, int width, int height) { - glBindTexture(GL_TEXTURE_2D, texture); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 解决glTexImage2D崩溃问题 - glTexImage2D(GL_TEXTURE_2D, 0, 3, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); -} - -void Canvas::SetPage(Prj::Page& page) { - wxImage img = prj->LoadPage(page); - if (!img.IsOk()) { - return; - } - if (IsLoaded()) { - scaleModel.ImageResize(img.GetSize()); - } else { - scaleModel = ImageScaleModel{img.GetSize(), this->GetClientSize()}; - } - this->page = &page; - - SetCurrent(*context); - if (texture) glDeleteTextures(1, &texture); - glGenTextures(1, &texture); - SetTextrue(texture, img.GetData(), img.GetWidth(), img.GetHeight()); - Refresh(); -} - -void Canvas::Clear() { - if (texture) glDeleteTextures(1, &texture); - prj = nullptr; - page = nullptr; - Refresh(); -} - -void Canvas::ZoomIn() { - if (!IsLoaded()) return; - scaleModel.Scale(Config::zoom_in_rate); - Refresh(); -} - -void Canvas::ZoomOut() { - if (!IsLoaded()) return; - scaleModel.Scale(Config::zoom_out_rate); - Refresh(); -} - -void Canvas::ZoomFit() { - if (!IsLoaded()) return; - scaleModel.ScaleTo(scaleModel.GetScaleSuitesPage()); - scaleModel.MoveToCenter(); - Refresh(); -} - -void Canvas::Zoom(double scale) { - if (IsLoaded()) { - scaleModel.ScaleTo(scale); - Refresh(); - } -} - -void Canvas::UpdateScrollbars() { - if (!IsLoaded()) { - SetScrollbar(wxHORIZONTAL, -1, -1, -1); - SetScrollbar(wxVERTICAL, -1, -1, -1); - return; - } - if (scaleModel.scaledSize.GetWidth() > scaleModel.windowSize.GetWidth()) { - SetScrollbar(wxHORIZONTAL, -scaleModel.offset.x, scaleModel.windowSize.GetWidth(), - scaleModel.scaledSize.GetWidth()); - } else { - SetScrollbar(wxHORIZONTAL, -1, -1, -1); - } - if (scaleModel.scaledSize.GetHeight() > scaleModel.windowSize.GetHeight()) { - SetScrollbar(wxVERTICAL, -scaleModel.offset.y, scaleModel.windowSize.GetHeight(), - scaleModel.scaledSize.GetHeight()); - } else { - SetScrollbar(wxVERTICAL, -1, -1, -1); - } -} - -static void InitGL() { - // 设置OpenGL状态 - glClearColor(0.0F, 0.0F, 0.0F, 1.0F); - glEnable(GL_DEPTH_TEST); - glEnable(GL_TEXTURE_2D); - // 设置深度测试函数 - glDepthFunc(GL_LEQUAL); - // GL_SMOOTH(光滑着色)/GL_FLAT(恒定着色) - glShadeModel(GL_SMOOTH); - // 开启混合 - glEnable(GL_BLEND); - // 设置混合函数 - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_ALPHA_TEST); // 启用Alpha测试 - // 设置Alpha测试条件为大于0.05则通过 - glAlphaFunc(GL_GREATER, 0.05); - // 设置逆时针索引为正面(GL_CCW/GL_CW) - glFrontFace(GL_CW); - // 开启线段反走样 - glEnable(GL_LINE_SMOOTH); - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); -} - -void UpdateProjection(wxSize size) { - if (size.GetWidth() <= 0 || size.GetHeight() <= 0) return; - - glViewport(0, 0, size.GetWidth(), size.GetHeight()); - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glOrtho(0, size.GetWidth(), size.GetHeight(), 0, -1, 1); -} - -void Canvas::OnPaint([[maybe_unused]] wxPaintEvent& event) { - wxPaintDC dc(this); - - SetCurrent(*context); - - // 初始化OpenGL(首次绘制时执行) - if (!initialized) { - InitGL(); - initialized = true; - } - - // 清除缓冲区 - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - // 设置正交投影(模拟视口坐标系) - UpdateProjection(GetClientSize()); - - // 设置投影(透视投影) - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - glTranslated(scaleModel.offset.x, scaleModel.offset.y, 0); - glScaled(scaleModel.scale, scaleModel.scale, 1.0); - - if (IsLoaded()) { - // 绑定纹理 - wxSize size = scaleModel.imageSize; - glBegin(GL_QUADS); - glColor3d(1.0, 1.0, 1.0); - glBindTexture(GL_TEXTURE_2D, texture); - glTexCoord2d(0, 0); - glVertex2d(0, 0); - glTexCoord2d(1, 0); - glVertex2d(size.GetWidth(), 0); - glTexCoord2d(1, 1); - glVertex2d(size.GetWidth(), size.GetHeight()); - glTexCoord2d(0, 1); - glVertex2d(0, size.GetHeight()); - glEnd(); - - UpdateScrollbars(); - - glColor4d(0.8, 0.84, 0.8, 0.25); - for (const wxRect& area : prj->GetSelectArea(*page)) { - glBegin(GL_QUADS); - glVertex2d(area.GetLeft(), area.GetTop()); - glVertex2d(area.GetLeft(), area.GetBottom()); - glVertex2d(area.GetRight(), area.GetBottom()); - glVertex2d(area.GetRight(), area.GetTop()); - glEnd(); - } - - auto DrawLine = [&size, this](int width, double line_y) { - double w = FromDIP(width) / scaleModel.scale; - glBegin(GL_QUADS); - glVertex2d(0, line_y - w); - glVertex2d(0, line_y + w); - glVertex2d(size.GetWidth(), line_y + w); - glVertex2d(size.GetWidth(), line_y - w); - glEnd(); - }; - - // draw crop lines - std::optional deleting_line; - if (is_deleting && mouse_position) { - int y = mouse_position->y; - y = std::lround(scaleModel.ReverseTransformY(y)); - auto search_line = - page->SearchNearestLine(y, FromDIP(static_cast(5.0 / scaleModel.scale) + 1)); - if (search_line) { - deleting_line = **search_line; - glColor4d(0.15, 0.58, 0.36, 0.5); - DrawLine(4, *deleting_line); - } - } - - glColor4d(0.30, 0.74, 0.52, 0.5); - for (int line : page->GetCropLines()) { - if (deleting_line == line) continue; - DrawLine(2, line); - } - - // draw mouse line - if (mouse_position) { - if (is_deleting) - glColor4d(0.90, 0.08, 0, 0.5); - else - glColor4d(0.34, 0.61, 0.84, 0.5); - double y = scaleModel.ReverseTransformY(mouse_position->y); - DrawLine(2, y); - } - } - - SwapBuffers(); -} - -void Canvas::OnSize(wxSizeEvent& event) { - if (context) { - SetCurrent(*context); - UpdateProjection(GetClientSize()); - } - if (IsLoaded()) scaleModel.WindowResize(event.GetSize()); - Refresh(); -} - -void Canvas::OnMouseWheel(wxMouseEvent& event) { - if (!IsLoaded()) return; - - double factor; - if (event.GetWheelRotation() > 0) { // zoom in - factor = Config::zoom_in_rate; - } else { // zoom out - factor = Config::zoom_out_rate; - } - scaleModel.Scale(factor, event.GetPosition()); - Refresh(); -} - -void Canvas::OnMouseLeftDown(wxMouseEvent& event) { - if (!is_mouse_capture) { - CaptureMouse(); - is_mouse_capture = true; - } - // m_parent->GetParent()->SetFocus(); - SetFocus(); - mouse_drag_start = event.GetPosition(); -} - -void Canvas::OnMouseLeftUp([[maybe_unused]] wxMouseEvent& event) { - if (is_mouse_capture) { - ReleaseMouse(); - is_mouse_capture = false; - } - mouse_drag_start.reset(); -} - -void Canvas::OnMouseLeftUp([[maybe_unused]] wxMouseCaptureLostEvent& event) { - if (is_mouse_capture) { - ReleaseMouse(); - is_mouse_capture = false; - } - mouse_drag_start.reset(); -} - -void Canvas::OnMouseRightDown(wxMouseEvent& event) { - SetFocus(); - event.Skip(); -} - -void Canvas::OnMouseRightUp(wxMouseEvent& event) { - if (!IsLoaded()) return; - - wxPoint mouse_position = event.GetPosition(); - if (!scaleModel.IsInsideImage(mouse_position)) return; - - int y = mouse_position.y; - y = std::lround(scaleModel.ReverseTransformY(y)); - if (is_deleting) { - std::optional::iterator> it = - page->SearchNearestLine(y, FromDIP(static_cast(5.0 / scaleModel.scale) + 1)); - if (it) prj->Execute(Prj::EraseLineRecord(*it, *page)); - } else { - prj->Execute(Prj::InsertLineRecord(y, *page)); - } - Refresh(); -} - -void Canvas::OnMouseMotion(wxMouseEvent& event) { - if (!IsLoaded()) return; - wxPoint mouse_drag = event.GetPosition(); - if (mouse_drag_start && event.Dragging()) { - auto dr = mouse_drag - *mouse_drag_start; - scaleModel.Move(dr); - *mouse_drag_start = mouse_drag; - Refresh(); - } - - // if mouse inside image - wxPoint mouse_position = event.GetPosition(); - if (scaleModel.IsInsideImage(mouse_position)) { - this->mouse_position = mouse_position; - Refresh(); - } else if (this->mouse_position) { - this->mouse_position = std::nullopt; - Refresh(); - } -} - -void Canvas::OnScroll(wxScrollWinEvent& event) { - if (!IsLoaded()) return; - - const int orientation = event.GetOrientation() & wxORIENTATION_MASK; - switch (orientation & wxORIENTATION_MASK) { - case wxHORIZONTAL: { - const int pos0 = GetScrollPos(wxHORIZONTAL); - const int pos1 = event.GetPosition(); - scaleModel.Move(wxPoint{pos0 - pos1, 0}); - Refresh(); - return; - } - case wxVERTICAL: { - const int pos0 = GetScrollPos(wxVERTICAL); - const int pos1 = event.GetPosition(); - scaleModel.Move(wxPoint{0, pos0 - pos1}); - Refresh(); - return; - } - // default: - // return; // ignore other orientations - } -} - -void Canvas::OnKeyUp(wxKeyEvent& event) { - switch (event.GetKeyCode()) { - case 'D': - if (is_deleting) { - is_deleting = false; - Refresh(); - } - break; - } -} - -void Canvas::OnKeyDown(wxKeyEvent& event) { - switch (event.GetKeyCode()) { - case 'D': - if (!is_deleting) { - is_deleting = true; - Refresh(); - } - break; - } -} - -void Canvas::OnKillFocus(wxFocusEvent& event) { - if (is_deleting) { - is_deleting = false; - Refresh(); - } - if (is_mouse_capture) { - ReleaseMouse(); - is_mouse_capture = false; - } - event.Skip(); -} - -// clang-format off -wxBEGIN_EVENT_TABLE(Canvas, wxPanel) - EVT_PAINT(Canvas::OnPaint) - EVT_SIZE(Canvas::OnSize) - - EVT_MOUSEWHEEL(Canvas::OnMouseWheel) - EVT_LEFT_DOWN(Canvas::OnMouseLeftDown) - EVT_LEFT_UP(Canvas::OnMouseLeftUp) - EVT_RIGHT_DOWN(Canvas::OnMouseRightDown) - EVT_RIGHT_UP(Canvas::OnMouseRightUp) - EVT_MOUSE_CAPTURE_LOST(Canvas::OnMouseLeftUp) - EVT_MOTION(Canvas::OnMouseMotion) - - EVT_SCROLLWIN(Canvas::OnScroll) - - EVT_KEY_DOWN(Canvas::OnKeyDown) - EVT_KEY_UP(Canvas::OnKeyUp) - - EVT_KILL_FOCUS(Canvas::OnKillFocus) -wxEND_EVENT_TABLE(); +#include "ui/Canvas.hpp" + +#include + +using namespace Croplines; + +Canvas::Canvas(wxWindow* parent, wxWindowID id) + : wxGLCanvas(parent, id, nullptr, wxDefaultPosition, wxDefaultSize, + wxFULL_REPAINT_ON_RESIZE | wxVSCROLL | wxHSCROLL) { + AlwaysShowScrollbars(); + + // disable on default + SetScrollbar(wxHORIZONTAL, -1, -1, -1); + SetScrollbar(wxVERTICAL, -1, -1, -1); + + context = new wxGLContext(this); +} + +Canvas::~Canvas() { + if (texture) glDeleteTextures(1, &texture); + delete context; +} + +static void SetTextrue(GLuint texture, void* pixels, int width, int height) { + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 解决glTexImage2D崩溃问题 + glTexImage2D(GL_TEXTURE_2D, 0, 3, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); +} + +void Canvas::SetPage(Page& page) { + wxImage img = page.getImage(); + if (!img.IsOk()) { + return; + } + if (IsLoaded()) { + m_scaleModel.OnImageResize(img.GetSize()); + } else { + m_scaleModel = ImageScaleModel{img.GetSize(), this->GetClientSize()}; + } + m_page = &page; + + SetCurrent(*context); + if (texture) glDeleteTextures(1, &texture); + glGenTextures(1, &texture); + SetTextrue(texture, img.GetData(), img.GetWidth(), img.GetHeight()); + Refresh(); +} + +void Canvas::Clear() { + if (texture) glDeleteTextures(1, &texture); + m_page = nullptr; + Refresh(); +} + +void Canvas::ZoomIn() { + if (!IsLoaded()) return; + m_scaleModel.Scale(ImageScaleModel::ZOOM_IN_RATE); + Refresh(); +} + +void Canvas::ZoomOut() { + if (!IsLoaded()) return; + m_scaleModel.Scale(ImageScaleModel::ZOOM_OUT_RATE); + Refresh(); +} + +void Canvas::ZoomFit() { + if (!IsLoaded()) return; + m_scaleModel.ScaleTo(m_scaleModel.GetScaleSuitesPage()); + m_scaleModel.MoveToCenter(); + Refresh(); +} + +void Canvas::Zoom(double scale) { + if (IsLoaded()) { + m_scaleModel.ScaleTo(scale); + Refresh(); + } +} + +void Canvas::UpdateScrollbars() { + if (!IsLoaded()) { + SetScrollbar(wxHORIZONTAL, -1, -1, -1); + SetScrollbar(wxVERTICAL, -1, -1, -1); + return; + } + if (m_scaleModel.scaledSize.GetWidth() > m_scaleModel.windowSize.GetWidth()) { + SetScrollbar(wxHORIZONTAL, -m_scaleModel.offset.x, m_scaleModel.windowSize.GetWidth(), + m_scaleModel.scaledSize.GetWidth()); + } else { + SetScrollbar(wxHORIZONTAL, -1, -1, -1); + } + if (m_scaleModel.scaledSize.GetHeight() > m_scaleModel.windowSize.GetHeight()) { + SetScrollbar(wxVERTICAL, -m_scaleModel.offset.y, m_scaleModel.windowSize.GetHeight(), + m_scaleModel.scaledSize.GetHeight()); + } else { + SetScrollbar(wxVERTICAL, -1, -1, -1); + } +} + +static void InitGL() { + // 设置OpenGL状态 + glClearColor(0.0F, 0.0F, 0.0F, 1.0F); + glEnable(GL_DEPTH_TEST); + glEnable(GL_TEXTURE_2D); + // 设置深度测试函数 + glDepthFunc(GL_LEQUAL); + // GL_SMOOTH(光滑着色)/GL_FLAT(恒定着色) + glShadeModel(GL_SMOOTH); + // 开启混合 + glEnable(GL_BLEND); + // 设置混合函数 + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_ALPHA_TEST); // 启用Alpha测试 + // 设置Alpha测试条件为大于0.05则通过 + glAlphaFunc(GL_GREATER, 0.05); + // 设置逆时针索引为正面(GL_CCW/GL_CW) + glFrontFace(GL_CW); + // 开启线段反走样 + glEnable(GL_LINE_SMOOTH); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); +} + +void UpdateProjection(wxSize size) { + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) return; + + glViewport(0, 0, size.GetWidth(), size.GetHeight()); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0, size.GetWidth(), size.GetHeight(), 0, -1, 1); +} + +void Canvas::OnPaint(wxPaintEvent&) { + wxPaintDC dc(this); + + SetCurrent(*context); + + // 初始化OpenGL(首次绘制时执行) + if (!initialized) { + InitGL(); + initialized = true; + } + + // 清除缓冲区 + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // 设置正交投影(模拟视口坐标系) + UpdateProjection(GetClientSize()); + + // 设置投影(透视投影) + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glTranslated(m_scaleModel.offset.x, m_scaleModel.offset.y, 0); + glScaled(m_scaleModel.scale, m_scaleModel.scale, 1.0); + + if (IsLoaded()) { + // 绑定纹理 + wxSize size = m_scaleModel.imageSize; + glBegin(GL_QUADS); + glColor3d(1.0, 1.0, 1.0); + glBindTexture(GL_TEXTURE_2D, texture); + glTexCoord2d(0, 0); + glVertex2d(0, 0); + glTexCoord2d(1, 0); + glVertex2d(size.GetWidth(), 0); + glTexCoord2d(1, 1); + glVertex2d(size.GetWidth(), size.GetHeight()); + glTexCoord2d(0, 1); + glVertex2d(0, size.GetHeight()); + glEnd(); + + UpdateScrollbars(); + + glColor4d(0.8, 0.84, 0.8, 0.25); + for (const wxRect& area : m_page->getSelectAreas()) { + glBegin(GL_QUADS); + glVertex2d(area.GetLeft(), area.GetTop()); + glVertex2d(area.GetLeft(), area.GetBottom()); + glVertex2d(area.GetRight(), area.GetBottom()); + glVertex2d(area.GetRight(), area.GetTop()); + glEnd(); + } + + auto DrawLine = [&size, this](int width, double line_y) { + double w = FromDIP(width) / m_scaleModel.scale; + glBegin(GL_QUADS); + glVertex2d(0, line_y - w); + glVertex2d(0, line_y + w); + glVertex2d(size.GetWidth(), line_y + w); + glVertex2d(size.GetWidth(), line_y - w); + glEnd(); + }; + + // draw crop lines + std::optional deleting_line; + if (is_deleting && mouse_position) { + int y = mouse_position->y; + y = std::lround(m_scaleModel.ReverseTransformY(y)); + + auto search_line = m_page->SearchNearestLine( + y, FromDIP(static_cast(5.0 / m_scaleModel.scale) + 1)); + if (search_line.has_value()) { + deleting_line = *search_line; + glColor4d(0.15, 0.58, 0.36, 0.5); + DrawLine(4, *deleting_line); + } + } + + glColor4d(0.30, 0.74, 0.52, 0.5); + for (int line : m_page->getCropLines()) { + if (deleting_line == line) continue; + DrawLine(2, line); + } + + // draw mouse line + if (mouse_position) { + if (is_deleting) + glColor4d(0.90, 0.08, 0, 0.5); + else + glColor4d(0.34, 0.61, 0.84, 0.5); + double y = m_scaleModel.ReverseTransformY(mouse_position->y); + DrawLine(2, y); + } + } + + SwapBuffers(); +} + +void Canvas::OnSize(wxSizeEvent& event) { + if (context) { + SetCurrent(*context); + UpdateProjection(GetClientSize()); + } + if (IsLoaded()) m_scaleModel.OnWindowResize(event.GetSize()); + Refresh(); +} + +void Canvas::OnMouseWheel(wxMouseEvent& event) { + if (!IsLoaded()) return; + + double factor; + if (event.GetWheelRotation() > 0) { // zoom in + factor = ImageScaleModel::ZOOM_IN_RATE; + } else { // zoom out + factor = ImageScaleModel::ZOOM_OUT_RATE; + } + m_scaleModel.Scale(factor, event.GetPosition()); + Refresh(); +} + +void Canvas::OnMouseLeftDown(wxMouseEvent& event) { + if (!IsLoaded()) return; + + if (!is_mouse_capture) { + CaptureMouse(); + is_mouse_capture = true; + } + // m_parent->GetParent()->SetFocus(); + SetFocus(); + mouse_drag_start = event.GetPosition(); +} + +void Canvas::OnMouseLeftUp(wxMouseEvent&) { + if (!IsLoaded()) return; + + if (is_mouse_capture) { + ReleaseMouse(); + is_mouse_capture = false; + } + mouse_drag_start.reset(); +} + +void Canvas::OnMouseLeftUp(wxMouseCaptureLostEvent&) { + if (!IsLoaded()) return; + + if (is_mouse_capture) { + ReleaseMouse(); + is_mouse_capture = false; + } + mouse_drag_start.reset(); +} + +void Canvas::OnMouseRightDown(wxMouseEvent& event) { + if (!IsLoaded()) return; + + SetFocus(); + event.Skip(); +} + +struct EraseLineCommand final : public wxCommand { + Page& page; + int line; + EraseLineCommand(Page& page, int line) + : wxCommand(true, wxT("删除直线")), page(page), line(line) {} + bool Do() override { return page.EraseLine(line); } + bool Undo() override { return page.InsertLine(line); } +}; + +struct InsertLineCommand final : public wxCommand { + Page& page; + int line; + InsertLineCommand(Page& page, int line) + : wxCommand(true, wxT("添加直线")), page(page), line(line) {} + bool Do() override { return page.InsertLine(line); } + bool Undo() override { return page.EraseLine(line); } +}; + +void Canvas::OnMouseRightUp(wxMouseEvent& event) { + if (!IsLoaded()) return; + + wxPoint mousePosition = event.GetPosition(); + if (!m_scaleModel.IsInsideImage(mousePosition)) return; + + int y = mousePosition.y; + y = std::lround(m_scaleModel.ReverseTransformY(y)); + if (is_deleting) { + int threshold = static_cast(5.0 / m_scaleModel.scale) + 1; + auto line = m_page->SearchNearestLine(y, FromDIP(threshold)); + if (line.has_value()) GetProcessor()->Submit(new EraseLineCommand{GetPage(), *line}); + } else { + GetProcessor()->Submit(new InsertLineCommand{GetPage(), y}); + } + Refresh(); +} + +void Canvas::OnMouseMotion(wxMouseEvent& event) { + if (!IsLoaded()) return; + + wxPoint mouse_drag = event.GetPosition(); + if (mouse_drag_start && event.Dragging()) { + auto dr = mouse_drag - *mouse_drag_start; + m_scaleModel.Move(dr); + *mouse_drag_start = mouse_drag; + Refresh(); + } + + // if mouse inside image + wxPoint mouse_position = event.GetPosition(); + if (m_scaleModel.IsInsideImage(mouse_position)) { + this->mouse_position = mouse_position; + Refresh(); + } else if (this->mouse_position) { + this->mouse_position = std::nullopt; + Refresh(); + } +} + +void Canvas::OnScroll(wxScrollWinEvent& event) { + if (!IsLoaded()) return; + + const int orientation = event.GetOrientation() & wxORIENTATION_MASK; + switch (orientation & wxORIENTATION_MASK) { + case wxHORIZONTAL: { + const int pos0 = GetScrollPos(wxHORIZONTAL); + const int pos1 = event.GetPosition(); + m_scaleModel.Move(wxPoint{pos0 - pos1, 0}); + Refresh(); + return; + } + case wxVERTICAL: { + const int pos0 = GetScrollPos(wxVERTICAL); + const int pos1 = event.GetPosition(); + m_scaleModel.Move(wxPoint{0, pos0 - pos1}); + Refresh(); + return; + } + default: + return; // ignore other orientations + } +} + +void Canvas::OnKeyUp(wxKeyEvent& event) { + if (!IsLoaded()) return; + + switch (event.GetKeyCode()) { + case 'D': + if (is_deleting) { + is_deleting = false; + Refresh(); + } + break; + } +} + +void Canvas::OnKeyDown(wxKeyEvent& event) { + if (!IsLoaded()) return; + + switch (event.GetKeyCode()) { + case 'D': + if (!is_deleting) { + is_deleting = true; + Refresh(); + } + break; + } +} + +void Canvas::OnKillFocus(wxFocusEvent& event) { + if (!IsLoaded()) return; + + if (is_deleting) { + is_deleting = false; + Refresh(); + } + if (is_mouse_capture) { + ReleaseMouse(); + is_mouse_capture = false; + } + event.Skip(); +} + +// clang-format off +wxBEGIN_EVENT_TABLE(Canvas, wxPanel) + EVT_PAINT(Canvas::OnPaint) + EVT_SIZE(Canvas::OnSize) + + EVT_MOUSEWHEEL(Canvas::OnMouseWheel) + EVT_LEFT_DOWN(Canvas::OnMouseLeftDown) + EVT_LEFT_UP(Canvas::OnMouseLeftUp) + EVT_RIGHT_DOWN(Canvas::OnMouseRightDown) + EVT_RIGHT_UP(Canvas::OnMouseRightUp) + EVT_MOUSE_CAPTURE_LOST(Canvas::OnMouseLeftUp) + EVT_MOTION(Canvas::OnMouseMotion) + + EVT_SCROLLWIN(Canvas::OnScroll) + + EVT_KEY_DOWN(Canvas::OnKeyDown) + EVT_KEY_UP(Canvas::OnKeyUp) + + EVT_KILL_FOCUS(Canvas::OnKillFocus) +wxEND_EVENT_TABLE(); diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp new file mode 100644 index 0000000..88264be --- /dev/null +++ b/src/ui/Canvas.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +#include "core/Document.hpp" +#include "core/ImageScaleModel.hpp" +#include "core/Page.hpp" + +namespace Croplines { +class Canvas : public wxGLCanvas { + public: + Canvas(wxWindow* parent, wxWindowID id); + ~Canvas() override; + void SetPage(Page& page); + Page& GetPage() { return *m_page; } + + Document& GetDocument() { return GetPage().getDocument(); } + wxCommandProcessor* GetProcessor() { return GetDocument().GetProcessor(); } + + void Clear(); + bool IsLoaded() const { return m_page != nullptr; } + ImageScaleModel& getScaleModel() { return m_scaleModel; } + + void ZoomIn(); + void ZoomOut(); + void ZoomFit(); + void Zoom(double scale); + + private: + bool is_deleting = false; + bool initialized = false; + + bool is_mouse_capture = false; + std::optional mouse_drag_start; + std::optional mouse_position; + + wxBitmap drawBmp; + + Page* m_page = nullptr; + ImageScaleModel m_scaleModel; + + wxGLContext* context; + GLuint texture; + cv::UMat uimageSrc; + wxImage imageDst; + bool imageModified = false; + + private: + void UpdateScrollbars(); + + void OnPaint(wxPaintEvent& event); + void OnSize(wxSizeEvent& event); + + void OnMouseWheel(wxMouseEvent& event); + void OnMouseLeftDown(wxMouseEvent& event); + void OnMouseLeftUp(wxMouseEvent& event); + void OnMouseLeftUp(wxMouseCaptureLostEvent& event); + void OnMouseRightDown(wxMouseEvent& event); + void OnMouseRightUp(wxMouseEvent& event); + void OnMouseMotion(wxMouseEvent& event); + + void OnScroll(wxScrollWinEvent& event); + + void OnKeyDown(wxKeyEvent& event); + void OnKeyUp(wxKeyEvent& event); + + void OnKillFocus(wxFocusEvent& event); + + wxDECLARE_EVENT_TABLE(); +}; +} // namespace Croplines diff --git a/src/ui/ConfigPanel.cpp b/src/ui/ConfigPanel.cpp new file mode 100644 index 0000000..c8cbd45 --- /dev/null +++ b/src/ui/ConfigPanel.cpp @@ -0,0 +1,58 @@ +#include "ui/ConfigPanel.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/DocumentData.hpp" +#include "ui/Defs.hpp" + +using namespace Croplines; + +ConfigPanel::ConfigPanel(wxWindow* parent, wxWindowID id) : wxNotebook(parent, id) { + m_processPage = new ProcessConfigPage{this, wxID_ANY}; + m_outputPage = new OutputConfigPage{this, wxID_ANY}; + + AddPage(m_processPage, wxT("处理")); + AddPage(m_outputPage, wxT("输出")); +} + +ProcessConfigPage::ProcessConfigPage(wxWindow* parent, wxWindowID id) : wxPanel(parent, id) { + m_sliderPixFilter = + new SliderWithSpin{this, sldid_cfg_PIX_FILTER, wxT("忽略斑点直径"), 8, 0, 50}; + + auto* bSizer = new wxBoxSizer{wxVERTICAL}; + bSizer->Add(m_sliderPixFilter, 0, wxEXPAND, 5); + + SetSizer(bSizer); + Layout(); + bSizer->Fit(this); +} + +OutputConfigPage::OutputConfigPage(wxWindow* parent, wxWindowID id) : wxPanel(parent, id) { + m_sliderBorder = new SliderWithSpin{this, sldid_cfg_BORDER, wxT("空白边距"), 10, 0, 100}; + + auto* bSizer = new wxBoxSizer{wxVERTICAL}; + bSizer->Add(m_sliderBorder, 0, wxEXPAND, 5); + + SetSizer(bSizer); + Layout(); + bSizer->Fit(this); +} + +void ConfigPanel::SyncUI(const DocumentConfig& config) { + m_processPage->SyncUI(config); + m_outputPage->SyncUI(config); +} + +void ProcessConfigPage::SyncUI(const DocumentConfig& config) { + m_sliderPixFilter->SetValue(config.filter_noise_size); +} + +void OutputConfigPage::SyncUI(const DocumentConfig& config) { + m_sliderBorder->SetValue(config.border); +} diff --git a/src/ui/ConfigPanel.hpp b/src/ui/ConfigPanel.hpp new file mode 100644 index 0000000..e262f65 --- /dev/null +++ b/src/ui/ConfigPanel.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include "core/DocumentData.hpp" +#include "ui/components/SliderWithSpin.hpp" + +namespace Croplines { + +class ProcessConfigPage; +class OutputConfigPage; + +class ConfigPanel : public wxNotebook { + public: + ConfigPanel(wxWindow* parent, wxWindowID id); + + void SyncUI(const DocumentConfig& config); + + private: + ProcessConfigPage* m_processPage; + OutputConfigPage* m_outputPage; +}; + +class ProcessConfigPage : public wxPanel { + public: + ProcessConfigPage(wxWindow* parent, wxWindowID id); + + void SyncUI(const DocumentConfig& config); + + private: + SliderWithSpin* m_sliderPixFilter; +}; + +class OutputConfigPage : public wxPanel { + public: + OutputConfigPage(wxWindow* parent, wxWindowID id); + + void SyncUI(const DocumentConfig& config); + + private: + SliderWithSpin* m_sliderBorder; +}; + +} // namespace Croplines diff --git a/src/ui/Defs.hpp b/src/ui/Defs.hpp new file mode 100644 index 0000000..27c1b75 --- /dev/null +++ b/src/ui/Defs.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace Croplines { +enum { + buttonID_CROP_CURR_PAGE = 1000, + buttonID_CROP_ALL_PAGE, + + pnid_PAGE_LIST, + + sldid_cfg_PIX_FILTER, + sldid_cfg_BORDER +}; +} diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp new file mode 100644 index 0000000..18614fd --- /dev/null +++ b/src/ui/MainFrame.cpp @@ -0,0 +1,364 @@ +#include "ui/MainFrame.hpp" + +#include +#include + +// #include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/Document.hpp" +#include "ui/Canvas.hpp" +#include "ui/components/SliderWithSpin.hpp" +#include "ui/ConfigPanel.hpp" +#include "ui/Defs.hpp" +#include "ui/ToolBar.hpp" +#include "utils/Asserts.hpp" + +using namespace Croplines; + +MainFrame::MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, + const wxSize& size) + : wxFrame(parent, id, title, pos) { + SetClientSize(FromDIP(size)); + SetIcon(wxICON(MAIN_ICON)); + SetMenuBar(m_menuBar); + + m_mgr.SetManagedWindow(this); + + m_toolBar = new ToolBar{this, wxID_ANY}; + m_mgr.AddPane(m_toolBar, wxAuiPaneInfo() + .Top() + .CaptionVisible(false) + .CloseButton(false) + .PaneBorder(false) + .Movable(false) + .Dock() + .Resizable() + .FloatingSize(wxSize(-1, -1)) + .DockFixed(true) + .LeftDockable(false) + .RightDockable(false) + .Floatable(false) + .Layer(10) + .ToolbarPane()); + + m_statusBar = CreateStatusBar(1, wxSTB_SIZEGRIP, wxID_ANY); + + auto* m_canvasPanel = new wxPanel{this, wxID_ANY}; + m_mgr.AddPane(m_canvasPanel, wxAuiPaneInfo() + .Left() + .CaptionVisible(false) + .PinButton(true) + .Dock() + .Resizable() + .FloatingSize(wxDefaultSize) + .CentrePane()); + auto* bSizer = new wxBoxSizer{wxVERTICAL}; + m_canvas = new Canvas{m_canvasPanel, wxID_ANY}; + bSizer->Add(m_canvas, 1, wxEXPAND, 5); + m_canvasPanel->SetSizer(bSizer); + m_canvasPanel->Layout(); + bSizer->Fit(m_canvasPanel); + + m_configPanel = new ConfigPanel{this, wxID_ANY}; + m_mgr.AddPane(m_configPanel, wxAuiPaneInfo() + .Right() + .Caption(wxT("设置")) + .PinButton(true) + .Dock() + .Resizable() + .FloatingSize(wxDefaultSize) + .BottomDockable(false) + .TopDockable(false) + .BestSize(FromDIP(wxSize(350, 500))) + .MinSize(FromDIP(wxSize(350, -1)))); + + m_pageListPanel = new wxListBox{this, pnid_PAGE_LIST, wxDefaultPosition, wxDefaultSize, + 0, nullptr, wxLB_NEEDED_SB}; + m_mgr.AddPane(m_pageListPanel, wxAuiPaneInfo() + .Left() + .Caption(wxT("页面列表")) + .PinButton(true) + .Dock() + .Resizable() + .FloatingSize(wxDefaultSize) + .BottomDockable(false) + .TopDockable(false) + .BestSize(FromDIP(wxSize(200, 500))) + .MinSize(FromDIP(wxSize(130, -1)))); + + m_mgr.Update(); + Center(wxBOTH); + + m_toolBar->Disable(); + m_configPanel->Disable(); + m_menuBar->Disable(); +} + +bool MainFrame::Load(std::filesystem::path path) { + if (m_doc.isLoad()) { + if (!Close()) return false; + } + + m_doc.Load(path); + m_toolBar->Enable(); + m_configPanel->Enable(); + m_menuBar->Enable(); + SyncUIPageListPanel(); + SyncUIConfigPanel(); + if (m_doc.PagesSize() != 0) CurrentPage(0); + SetTitle(wxString(path.filename())); + return true; +} + +static bool ShowCloseDialog(wxWindow* parent, Document& doc) { + wxMessageDialog dialog(parent, wxT("项目已更改,是否保存?"), + wxASCII_STR(wxMessageBoxCaptionStr), + wxICON_QUESTION | wxYES_NO | wxCANCEL); + switch (dialog.ShowModal()) { + case wxID_YES: + return doc.Save(); + case wxID_NO: + return true; + case wxID_CANCEL: + return false; + default: + UNREACHABLE(); + } +} + +bool MainFrame::Save() { + if (m_doc.isLoad()) { + m_doc.Save(); + return true; + } else + return false; +} + +bool MainFrame::Close() { + if (m_doc.isLoad()) { + if (m_doc.isModified()) { + if (!ShowCloseDialog(this, m_doc)) return false; + } + SetFocus(); // 防止焦点在需要禁用的组件中 + m_toolBar->Disable(); + m_menuBar->Disable(); + m_configPanel->Disable(); + m_doc.Close(); + m_canvas->Clear(); + m_pageListPanel->Clear(); + } + return true; +} + +void MainFrame::CurrentPage(std::size_t page) { + if (m_doc.isLoad()) { + m_currentPageIdx = std::clamp(page, static_cast(0), m_doc.PagesSize() - 1); + m_toolBar->EnableTool(wxID_UP, m_currentPageIdx != 0); + m_toolBar->EnableTool(wxID_DOWN, m_currentPageIdx != m_doc.PagesSize() - 1); + m_currentPage.emplace(m_doc.LoadPage(m_currentPageIdx)); + m_pageListPanel->SetSelection(static_cast(m_currentPageIdx)); + m_canvas->SetPage(*m_currentPage); + m_canvas->Refresh(); + } +} + +void MainFrame::SyncUIPageListPanel() { + m_pageListPanel->Clear(); + if (m_doc.isLoad() && m_doc.PagesSize() != 0) { + std::vector file_names; + file_names.reserve(m_doc.PagesSize()); + std::ranges::transform( + m_doc.getData().pages, std::back_inserter(file_names), + [](const std::unique_ptr& page) { return wxString(page->path.filename()); }); + + m_pageListPanel->Set(file_names); + } +} + +void MainFrame::SyncUIConfigPanel() { m_configPanel->SyncUI(m_doc.getConfig()); } + +void MainFrame::OnLoad(wxCommandEvent&) { + if (m_doc.isLoad() && m_doc.isModified()) { + if (!ShowCloseDialog(this, m_doc)) return; + } + wxDirDialog dir_dialog(this, wxT("选择目录"), wxEmptyString, + wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + auto result = dir_dialog.ShowModal(); + if (result == wxID_OK) { + wxString dir = dir_dialog.GetPath(); + Load(std::filesystem::path(dir.utf8_string())); + } +} + +void MainFrame::OnUndo(wxCommandEvent&) { + if (m_doc.isLoad()) { + m_doc.GetProcessor()->Undo(); + m_canvas->Refresh(); + } +} + +void MainFrame::OnRedo(wxCommandEvent&) { + if (m_doc.isLoad()) { + m_doc.GetProcessor()->Redo(); + m_canvas->Refresh(); + } +} + +void MainFrame::OnCropCurrPage(wxCommandEvent&) { + if (m_currentPage) { + SetStatusText(std::format("Page {} is croping...", CurrentPage() + 1)); + if (m_currentPage->SaveCrops()) { + SetStatusText(std::format("Page {} finished!", CurrentPage() + 1)); + } else { + SetStatusText(std::format("Page {} failed!", CurrentPage() + 1)); + } + } +} + +void MainFrame::OnCropAllPage(wxCommandEvent&) { + if (!m_doc.isLoad()) return; + + if (m_canvas->IsLoaded()) { + // std::thread t([this]() { + for (size_t i = 0; i < m_doc.PagesSize(); i++) { + Page page = m_doc.LoadPage(i); + SetStatusText(std::format("Page {} is croping...", i + 1)); + page.SaveCrops(); + } + SetStatusText(wxT("Croping finised!")); + // }); + // t.detach(); + // TODO! + } +} + +void MainFrame::OnExit(wxCloseEvent& event) { + if (Close()) + Destroy(); + else + event.Veto(); +} + +void MainFrame::OnAbout(wxCommandEvent&) { +#include "License.hpp" + wxAboutDialogInfo aboutInfo; + aboutInfo.SetName(wxT("Croplines")); + aboutInfo.SetIcon(wxICON(MAIN_ICON)); + aboutInfo.AddDeveloper(wxT("Likend")); + aboutInfo.SetWebSite(wxT("https://github.com/Likend/Croplines")); + aboutInfo.SetCopyright(wxT("(C) 2024-2026")); + aboutInfo.SetLicence(LICENSE); + wxAboutBox(aboutInfo); +} + +void MainFrame::OnClickListBox(wxCommandEvent& event) { + if (CurrentPage() != event.GetSelection()) { + CurrentPage(event.GetSelection()); + } +} + +template +class ChangeConfigCommand : public wxCommand { + public: + ChangeConfigCommand(SliderWithSpin* slider, Document& doc, T& placeToMidify, T newValue) + : wxCommand(true, wxT("更改设置")), + m_doc(doc), + m_slider(slider), + m_placeToModify(placeToMidify), + m_newValue(newValue), + m_oldValue(placeToMidify) { + std::cout << "ChangeConfigCommand " << m_oldValue << " to " << m_newValue << std::endl; + } + + bool Do() override { + std::cout << "Change current " << m_placeToModify << " from " << m_oldValue << " to " + << m_newValue << std::endl; + if (m_slider && m_slider->GetValue() != m_newValue) m_slider->SetValue(m_newValue); + m_placeToModify = m_newValue; + m_doc.setModified(); + return true; + } + + bool Undo() override { + std::cout << "Change current " << m_placeToModify << " from " << m_newValue << " to " + << m_oldValue << std::endl; + if (m_slider && m_slider->GetValue() != m_oldValue) m_slider->SetValue(m_oldValue); + m_placeToModify = m_oldValue; + m_doc.setModified(); + return true; + } + + private: + Document& m_doc; + SliderWithSpin* m_slider; + + T& m_placeToModify; + T m_newValue; + T m_oldValue; +}; + +void MainFrame::OnChnageCfgFilerPixSize(wxCommandEvent& event) { + wxWindow* win = FindWindowById(sldid_cfg_PIX_FILTER); + SliderWithSpin* slider = wxDynamicCast(win, SliderWithSpin); + auto* command = new ChangeConfigCommand{slider, m_doc, m_doc.getConfig().filter_noise_size, + event.GetInt()}; + m_doc.GetProcessor()->Submit(command); +} + +void MainFrame::OnChangeCfgBorder(wxCommandEvent& event) { + wxWindow* win = FindWindowById(sldid_cfg_BORDER); + SliderWithSpin* slider = wxDynamicCast(win, SliderWithSpin); + auto* command = + new ChangeConfigCommand{slider, m_doc, m_doc.getConfig().border, event.GetInt()}; + m_doc.GetProcessor()->Submit(command); +} + +void MainFrame::OnUpdateUndo(wxUpdateUIEvent&) { + m_menuBar->Enable(wxID_UNDO, m_doc.GetProcessor()->CanUndo()); +} +void MainFrame::OnUpdateRedo(wxUpdateUIEvent&) { + m_menuBar->Enable(wxID_REDO, m_doc.GetProcessor()->CanRedo()); +} +void MainFrame::OnUpdateSave(wxUpdateUIEvent&) { + m_menuBar->Enable(wxID_SAVE, m_doc.isModified()); + m_toolBar->EnableTool(wxID_SAVE, m_doc.isModified()); + m_toolBar->Refresh(); +} + +// clang-format off +wxBEGIN_EVENT_TABLE(MainFrame, wxFrame) + EVT_MENU(wxID_UP, MainFrame::OnPrevPage) + EVT_MENU(wxID_DOWN, MainFrame::OnNextPage) + EVT_MENU(wxID_SAVE, MainFrame::OnSave) + EVT_MENU(wxID_OPEN, MainFrame::OnLoad) + EVT_MENU(wxID_UNDO, MainFrame::OnUndo) + EVT_MENU(wxID_REDO, MainFrame::OnRedo) + EVT_MENU(wxID_ZOOM_IN, MainFrame::OnZoomIn) + EVT_MENU(wxID_ZOOM_OUT, MainFrame::OnZoomOut) + EVT_MENU(wxID_ZOOM_FIT, MainFrame::OnZoomFit) + EVT_MENU(wxID_ZOOM_100, MainFrame::OnZoom100) + EVT_MENU(buttonID_CROP_CURR_PAGE, MainFrame::OnCropCurrPage) + EVT_MENU(buttonID_CROP_ALL_PAGE, MainFrame::OnCropAllPage) + EVT_MENU(wxID_CLOSE, MainFrame::OnClose) + EVT_MENU(wxID_EXIT, MainFrame::OnExit) + EVT_MENU(wxID_ABOUT, MainFrame::OnAbout) + EVT_CLOSE(MainFrame::OnExit) + EVT_LISTBOX(pnid_PAGE_LIST, MainFrame::OnClickListBox) + EVT_SLIDER(sldid_cfg_PIX_FILTER, MainFrame::OnChnageCfgFilerPixSize) + EVT_SLIDER(sldid_cfg_BORDER, MainFrame::OnChangeCfgBorder) + + // Update ui + EVT_UPDATE_UI(wxID_UNDO, MainFrame::OnUpdateUndo) + EVT_UPDATE_UI(wxID_REDO, MainFrame::OnUpdateRedo) + EVT_UPDATE_UI(wxID_SAVE, MainFrame::OnUpdateSave) + +wxEND_EVENT_TABLE(); diff --git a/src/ui/MainFrame.hpp b/src/ui/MainFrame.hpp new file mode 100644 index 0000000..45368db --- /dev/null +++ b/src/ui/MainFrame.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/Document.hpp" +#include "ui/Canvas.hpp" +#include "ui/ConfigPanel.hpp" +#include "ui/MenuBar.hpp" +#include "ui/ToolBar.hpp" + +namespace Croplines { +class MainFrame final : public wxFrame { + public: + MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, + const wxSize& size); + ~MainFrame() override { m_mgr.UnInit(); } + + private: + wxAuiManager m_mgr; + + MenuBar* m_menuBar = new MenuBar{}; + ToolBar* m_toolBar; + wxStatusBar* m_statusBar; + Canvas* m_canvas; + wxListBox* m_pageListPanel; + ConfigPanel* m_configPanel; + + std::size_t m_currentPageIdx = 0; + Document m_doc; + std::optional m_currentPage; + + bool Load(std::filesystem::path path); + bool Save(); + bool Close(); + + std::size_t CurrentPage() { return m_currentPageIdx; } + void CurrentPage(std::size_t); + void PrevPage() { CurrentPage(CurrentPage() - 1); } + void NextPage() { CurrentPage(CurrentPage() + 1); } + + void SyncUIPageListPanel(); + void SyncUIConfigPanel(); + + void OnPrevPage(wxCommandEvent&) { PrevPage(); } + void OnNextPage(wxCommandEvent&) { NextPage(); } + void OnSave(wxCommandEvent&) { Save(); } + void OnLoad(wxCommandEvent&); + void OnUndo(wxCommandEvent&); + void OnRedo(wxCommandEvent&); + void OnZoomIn(wxCommandEvent&) { m_canvas->ZoomIn(); } + void OnZoomOut(wxCommandEvent&) { m_canvas->ZoomOut(); } + void OnZoomFit(wxCommandEvent&) { m_canvas->ZoomFit(); } + void OnZoom100(wxCommandEvent&) { m_canvas->Zoom(1.0); } + void OnCropCurrPage(wxCommandEvent&); + void OnCropAllPage(wxCommandEvent&); + void OnClose(wxCommandEvent&) { Close(); } + void OnExit(wxCloseEvent&); + void OnExit(wxCommandEvent&) { Close(); } + void OnAbout(wxCommandEvent&); + void OnClickListBox(wxCommandEvent&); + void OnChnageCfgFilerPixSize(wxCommandEvent& event); + void OnChangeCfgBorder(wxCommandEvent& event); + + // Update ui + void OnUpdateUndo(wxUpdateUIEvent&); + void OnUpdateRedo(wxUpdateUIEvent&); + void OnUpdateSave(wxUpdateUIEvent&); + + wxDECLARE_EVENT_TABLE(); +}; +} // namespace Croplines diff --git a/src/ui/MenuBar.cpp b/src/ui/MenuBar.cpp new file mode 100644 index 0000000..f46e348 --- /dev/null +++ b/src/ui/MenuBar.cpp @@ -0,0 +1,41 @@ +#include "ui/MenuBar.hpp" + +#include "ui/Defs.hpp" + +using namespace Croplines; + +MenuBar::MenuBar() : wxMenuBar() { + menu_file->Append(wxID_OPEN, wxT("&Load\tCtrl+O")); + menu_file->Append(wxID_SAVE); + menu_file->AppendSeparator(); + menu_file->Append(buttonID_CROP_CURR_PAGE, wxT("Crop ¤t page"), + wxT("Crop current page to subimages and save each one to " + "output directory")); + menu_file->Append(buttonID_CROP_ALL_PAGE, wxT("Crop &all pages"), + wxT("Crop all pages to subimages and save each one to " + "output directory")); + menu_file->AppendSeparator(); + menu_file->Append(wxID_CLOSE); + menu_file->Append(wxID_EXIT); + + Append(menu_file, wxT("&File")); + + menu_edit->Append(wxID_UNDO); + menu_edit->Append(wxID_REDO); + menu_edit->AppendSeparator(); + menu_edit->Append(wxID_UP, wxT("Last page\tUp"), wxT("Move to last page")); + menu_edit->Append(wxID_DOWN, wxT("Next page\tDown"), wxT("Move to next page")); + + Append(menu_edit, wxT("&Edit")); + + menu_view->Append(wxID_ZOOM_IN); + menu_view->Append(wxID_ZOOM_OUT); + menu_view->Append(wxID_ZOOM_FIT); + menu_view->Append(wxID_ZOOM_100); + + Append(menu_view, wxT("&View")); + + menu_help->Append(wxID_ABOUT); + + Append(menu_help, wxT("&Help")); +} diff --git a/src/ui/MenuBar.hpp b/src/ui/MenuBar.hpp new file mode 100644 index 0000000..94a51c0 --- /dev/null +++ b/src/ui/MenuBar.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace Croplines { +class MenuBar : public wxMenuBar { + public: + wxMenu* menu_file = new wxMenu{}; + wxMenu* menu_edit = new wxMenu{}; + wxMenu* menu_view = new wxMenu{}; + wxMenu* menu_help = new wxMenu{}; + + MenuBar(); +}; +} // namespace Croplines diff --git a/src/ui/ToolBar.cpp b/src/ui/ToolBar.cpp new file mode 100644 index 0000000..9b50370 --- /dev/null +++ b/src/ui/ToolBar.cpp @@ -0,0 +1,49 @@ +#include "ui/ToolBar.hpp" + +#include + +#include "ui/Defs.hpp" + +using namespace Croplines; + +ToolBar::ToolBar(wxWindow* parent, wxWindowID id) + : wxAuiToolBar(parent, id, wxDefaultPosition, wxDefaultSize, + wxAUI_TB_HORZ_LAYOUT | wxAUI_TB_OVERFLOW | wxAUI_TB_TEXT) { + SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + AddTool(wxID_UP, wxT("上一页"), wxBITMAP_PNG(IMG_PREV_PAGE), wxNullBitmap, wxITEM_NORMAL, + wxT("上一页"), wxT("上一页"), nullptr); + AddTool(wxID_DOWN, wxT("下一页"), wxBITMAP_PNG(IMG_NEXT_PAGE), wxNullBitmap, wxITEM_NORMAL, + wxT("下一页"), wxT("下一页"), nullptr); + + AddSeparator(); + + AddTool(wxID_SAVE, wxT("保存"), wxBITMAP_PNG(IMG_SAVE), wxNullBitmap, wxITEM_NORMAL, + wxT("保存"), wxT("保存"), nullptr); + AddTool(wxID_OPEN, wxT("载入"), wxBITMAP_PNG(IMG_LOAD), wxNullBitmap, wxITEM_NORMAL, + wxT("载入"), wxT("载入"), nullptr); + + AddSeparator(); + + AddTool(wxID_ZOOM_FIT, wxT("缩放合适大小"), wxBITMAP_PNG(IMG_ZOOM_PAGE), wxNullBitmap, + wxITEM_NORMAL, wxT("缩放合适大小"), wxT("缩放合适大小"), nullptr); + AddTool(buttonID_CROP_CURR_PAGE, wxT("裁剪当前页"), wxBITMAP_PNG(IMG_CROP_CURR_PAGE), + wxNullBitmap, wxITEM_NORMAL, wxT("裁剪当前页"), wxT("裁剪当前页"), nullptr); + AddTool(buttonID_CROP_ALL_PAGE, wxT("裁剪全部"), wxBITMAP_PNG(IMG_CROP_ALL_PAGE), wxNullBitmap, + wxITEM_NORMAL, wxT("裁剪全部"), wxT("裁剪全部"), nullptr); + + Realize(); +} + +bool ToolBar::Enable(bool state) { + EnableTool(wxID_UP, state); + EnableTool(wxID_DOWN, state); + EnableTool(wxID_SAVE, state); + // EnableTool(wxID_OPEN, state); + EnableTool(wxID_ZOOM_FIT, state); + EnableTool(buttonID_CROP_CURR_PAGE, state); + EnableTool(buttonID_CROP_ALL_PAGE, state); + Refresh(); + + return true; +} diff --git a/src/ui/ToolBar.hpp b/src/ui/ToolBar.hpp new file mode 100644 index 0000000..fcef77a --- /dev/null +++ b/src/ui/ToolBar.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +namespace Croplines { +class ToolBar final : public wxAuiToolBar { + public: + ToolBar(wxWindow* parent, wxWindowID id); + + bool Enable(bool state = true) override; +}; +} // namespace Croplines diff --git a/src/ui/components/SliderWithSpin.cpp b/src/ui/components/SliderWithSpin.cpp new file mode 100644 index 0000000..4b23743 --- /dev/null +++ b/src/ui/components/SliderWithSpin.cpp @@ -0,0 +1,90 @@ +#include "ui/components/SliderWithSpin.hpp" + +#include +#include + +using namespace Croplines; + +constexpr int SLIDER_ID = 1100; +constexpr int SPIN_ID = 1101; + +SliderWithSpin::SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, + int minValue, int maxValue, const wxPoint& pos, const wxSize& size) + : wxPanel(parent, id, pos, size) { + m_label = new wxStaticText(this, wxID_ANY, label); + m_slider = new wxSlider(this, SLIDER_ID, value, minValue, maxValue); + m_spin = new wxSpinCtrl(this, SPIN_ID, wxEmptyString, wxDefaultPosition, wxDefaultSize, + wxSP_ARROW_KEYS, minValue, maxValue, value); + + auto* bSizer = new wxBoxSizer(wxHORIZONTAL); + bSizer->Add(m_label, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + bSizer->Add(m_slider, 1, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5); + bSizer->Add(m_spin, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 5); + + SetSizer(bSizer); +} + +bool SliderWithSpin::Enable(bool enable) { + wxWindow* const items[] = {m_label, m_slider, m_spin}; + const bool prevEnable = IsEnabled(); + bool ret = wxPanel::Enable(enable); + if (!ret) { + return false; + } + for (const auto& item : items) { + ret = item->Enable(enable); + if (!ret) { + wxPanel::Enable(prevEnable); + for (const auto* it = items; it != &item; it++) { + (*it)->Enable(prevEnable); + } + return false; + } + } + return true; +} + +void SliderWithSpin::SetValue(int value) { + this->m_value = value; + m_spin->SetValue(value); + m_slider->SetValue(value); +} + +void SliderWithSpin::CallEvent(int value) { + wxCommandEvent evt(wxEVT_COMMAND_SLIDER_UPDATED, GetId()); + evt.SetEventObject(this); + evt.SetInt(value); + ProcessWindowEvent(evt); +} + +void SliderWithSpin::OnSliderChanging(wxCommandEvent&) { + const int value = m_slider->GetValue(); + if (m_spin->GetValue() != value) { + m_spin->SetValue(value); + } + this->m_value = value; +} + +void SliderWithSpin::OnSpinChanged(wxSpinEvent&) { + const int value = m_spin->GetValue(); + if (m_slider->GetValue() != value) { + m_slider->SetValue(value); + } + this->m_value = value; + CallEvent(value); +} + +void SliderWithSpin::OnSliderChanged(wxScrollEvent& event) { + const int value = m_slider->GetValue(); + CallEvent(value); + event.Skip(); +} + +IMPLEMENT_DYNAMIC_CLASS(SliderWithSpin, wxFrame); + +// clang-format off +wxBEGIN_EVENT_TABLE(SliderWithSpin, wxPanel) + EVT_SLIDER(SLIDER_ID, SliderWithSpin::OnSliderChanging) + EVT_SPINCTRL(SPIN_ID, SliderWithSpin::OnSpinChanged) + EVT_SCROLL_CHANGED(SliderWithSpin::OnSliderChanged) +wxEND_EVENT_TABLE(); diff --git a/src/ctrl.h b/src/ui/components/SliderWithSpin.hpp similarity index 55% rename from src/ctrl.h rename to src/ui/components/SliderWithSpin.hpp index 2330bc5..e636501 100644 --- a/src/ctrl.h +++ b/src/ui/components/SliderWithSpin.hpp @@ -1,45 +1,37 @@ -#pragma once -#include -#include - -namespace Croplines { -class SliderWithSpin : public wxPanel { - private: - wxStaticText* m_label; - wxSlider* m_slider; - wxSpinCtrl* m_spin; - - public: - SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, int minValue, - int maxValue, const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize); - - bool Enable(bool enable = false) override; - - private: - int value; - - public: - inline int GetValue() { return value; } - void SetValue(int value); - - private: - void CallEvent(int value); - - void OnSliderChanged(wxCommandEvent& event); - - void OnSpinChanged(wxSpinEvent& event); - - wxDECLARE_EVENT_TABLE(); -}; - -class MenuBar : public wxMenuBar { - public: - wxMenu* menu_file; - wxMenu* menu_edit; - wxMenu* menu_view; - wxMenu* menu_help; - - MenuBar(); -}; -} // namespace Croplines +#pragma once + +#include +#include +#include +#include +#include + +namespace Croplines { +class SliderWithSpin : public wxPanel { + public: + SliderWithSpin() : wxPanel() {} + SliderWithSpin(wxWindow* parent, wxWindowID id, const wxString& label, int value, int minValue, + int maxValue, const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize); + + bool Enable(bool enable = false) override; + + int GetValue() const { return m_value; } + void SetValue(int value); + + private: + wxStaticText* m_label; + wxSlider* m_slider; + wxSpinCtrl* m_spin; + int m_value; + + void CallEvent(int value); + void OnSliderChanging(wxCommandEvent&); + void OnSpinChanged(wxSpinEvent&); + void OnSliderScrollEnd(wxScrollEvent&); + void OnSliderChanged(wxScrollEvent&); + + wxDECLARE_EVENT_TABLE(); + wxDECLARE_DYNAMIC_CLASS_NO_COPY(SliderWithSpin); +}; +} // namespace Croplines diff --git a/src/utils/Asserts.hpp b/src/utils/Asserts.hpp new file mode 100644 index 0000000..6d29f26 --- /dev/null +++ b/src/utils/Asserts.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#define ANNOTATE_LOCATION __FUNCTION__, __FILE__, __LINE__ + +#ifndef NDEBUG + +namespace assert_detail { +template +[[noreturn]] static inline void assert_throw(std::string_view func_name, std::string_view file_name, + size_t file_line, const Arg&... args) { + std::stringstream ss; + ((ss << args << ' '), ...); + ss << '(' << func_name << " @ " << file_name << ':' << file_line << ')'; + throw std::runtime_error(ss.str()); +} +} // namespace assert_detail + +#define ASSERT_WITH(x, ...) \ + do { \ + if (x) { \ + } else { \ + ::assert_detail::assert_throw(ANNOTATE_LOCATION, __VA_ARGS__); \ + } \ + } while (0); + +#define ASSERT(x) ASSERT_WITH(x, "Assert failed!") +#define UNREACHABLE() ASSERT_WITH(false, "Unreachable!"); + +#else + +#define UNREACHABLE() __builtin_unreachable() +#define ASSERT_WITH(x, ...) \ + do { \ + if (x) { \ + } else { \ + UNREACHABLE(); \ + } \ + } +#define ASSERT(x) ASSERT_WITH(x) + +#endif diff --git a/src/utils/Compare.cpp b/src/utils/Compare.cpp new file mode 100644 index 0000000..9f1b807 --- /dev/null +++ b/src/utils/Compare.cpp @@ -0,0 +1,29 @@ +#include "utils/Compare.hpp" + +#include + +std::strong_ordering Croplines::NaturalCompare(std::string_view a, std::string_view b) { + for (const char *i1 = a.begin(), *i2 = b.begin(); i1 != a.end() && i2 != b.end(); ++i1, ++i2) { + if (std::isdigit(*i1) && std::isdigit(*i2)) { + const char *ii1 = i1, *ii2 = i2; + while (ii1 != a.end() && *ii1 == '0') ++ii1; + while (ii2 != b.end() && *ii2 == '0') ++ii2; + auto zero_count1 = std::distance(i1, ii1); + auto zero_count2 = std::distance(i2, ii2); + + i1 = ii1; + i2 = ii2; + while (ii1 != a.end() && std::isdigit(*ii1)) ++ii1; + while (ii2 != b.end() && std::isdigit(*ii2)) ++ii2; + auto num1 = std::string_view(i1, ii1 - i1); + auto num2 = std::string_view(i2, ii2 - i2); + + if (auto cmp = num1.length() <=> num2.length(); cmp != 0) return cmp; + if (auto cmp = num1 <=> num2; cmp != 0) return cmp; + if (auto cmp = zero_count1 <=> zero_count2; cmp != 0) return cmp; + } else { + return std::toupper(*i1) <=> std::toupper(*i2); + } + } + return std::strong_ordering::equal; +} diff --git a/src/utils/Compare.hpp b/src/utils/Compare.hpp new file mode 100644 index 0000000..cdf947b --- /dev/null +++ b/src/utils/Compare.hpp @@ -0,0 +1,6 @@ +#include +#include + +namespace Croplines { +std::strong_ordering NaturalCompare(std::string_view a, std::string_view b); +} diff --git a/src/wxUI.cpp b/src/wxUI.cpp deleted file mode 100644 index 9adfba8..0000000 --- a/src/wxUI.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "wxUI.h" - -#include - -using namespace Croplines; - -MainUI::MainUI(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, - const wxSize& size, long style) - : wxFrame(parent, id, title, pos, parent->FromDIP(size), style) { - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - m_mgr.SetManagedWindow(this); - m_mgr.SetFlags(wxAUI_MGR_DEFAULT); - - toolbar = new wxAuiToolBar(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, - wxAUI_TB_HORZ_LAYOUT | wxAUI_TB_OVERFLOW | wxAUI_TB_TEXT); - toolbar->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - - btn_prev_page = - toolbar->AddTool(wxID_UP, wxT("上一页"), wxBITMAP_PNG(IMG_PREV_PAGE), wxNullBitmap, - wxITEM_NORMAL, wxT("上一页"), wxT("上一页"), NULL); - - btn_next_page = - toolbar->AddTool(wxID_DOWN, wxT("下一页"), wxBITMAP_PNG(IMG_NEXT_PAGE), wxNullBitmap, - wxITEM_NORMAL, wxT("下一页"), wxT("下一页"), NULL); - - toolbar->AddSeparator(); - - btn_save = toolbar->AddTool(wxID_SAVE, wxT("保存"), wxBITMAP_PNG(IMG_SAVE), wxNullBitmap, - wxITEM_NORMAL, wxT("保存"), wxT("保存"), NULL); - - btn_load = toolbar->AddTool(wxID_OPEN, wxT("载入"), wxBITMAP_PNG(IMG_LOAD), wxNullBitmap, - wxITEM_NORMAL, wxT("载入"), wxT("载入"), NULL); - - toolbar->AddSeparator(); - - btn_zoom_page = toolbar->AddTool(wxID_ZOOM_FIT, wxT("缩放合适大小"), - wxBITMAP_PNG(IMG_ZOOM_PAGE), wxNullBitmap, wxITEM_NORMAL, - wxT("缩放合适大小"), wxT("缩放合适大小"), NULL); - - btn_crop_curr_page = - toolbar->AddTool(btnid_CROP_CURR_PAGE, wxT("裁剪当前页"), wxBITMAP_PNG(IMG_CROP_CURR_PAGE), - wxNullBitmap, wxITEM_NORMAL, wxT("裁剪当前页"), wxT("裁剪当前页"), NULL); - - btn_crop_all_page = - toolbar->AddTool(btnid_CROP_ALL_PAGE, wxT("裁剪全部"), wxBITMAP_PNG(IMG_CROP_ALL_PAGE), - wxNullBitmap, wxITEM_NORMAL, wxT("裁剪全部"), wxT("裁剪全部"), NULL); - - toolbar->Realize(); - m_mgr.AddPane(toolbar, wxAuiPaneInfo() - .Top() - .CaptionVisible(false) - .CloseButton(false) - .PaneBorder(false) - .Movable(false) - .Dock() - .Resizable() - .FloatingSize(wxSize(-1, -1)) - .DockFixed(true) - .LeftDockable(false) - .RightDockable(false) - .Floatable(false) - .Layer(10) - .ToolbarPane()); - - status_bar = this->CreateStatusBar(1, wxSTB_SIZEGRIP, wxID_ANY); - pn_page_list = new wxListBox(this, pnid_PAGE_LIST, wxDefaultPosition, wxDefaultSize, 0, NULL, - wxLB_NEEDED_SB); - m_mgr.AddPane(pn_page_list, wxAuiPaneInfo() - .Left() - .Caption(wxT("页面列表")) - .PinButton(true) - .Dock() - .Resizable() - .FloatingSize(wxDefaultSize) - .BottomDockable(false) - .TopDockable(false) - .BestSize(FromDIP(wxSize(200, 500))) - .MinSize(FromDIP(wxSize(130, -1)))); - - pn_canvas = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - m_mgr.AddPane(pn_canvas, wxAuiPaneInfo() - .Left() - .CaptionVisible(false) - .PinButton(true) - .Dock() - .Resizable() - .FloatingSize(wxDefaultSize) - .CentrePane()); - auto* bSizer31 = new wxBoxSizer(wxVERTICAL); - canvas = new Canvas(pn_canvas, wxID_ANY); - bSizer31->Add(canvas, 1, wxEXPAND, 5); - pn_canvas->SetSizer(bSizer31); - // pn_canvas->Layout(); - // bSizer31->Fit(pn_canvas); - - pn_config = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, wxT("设置")); - m_mgr.AddPane(pn_config, wxAuiPaneInfo() - .Right() - .Caption(wxT("设置")) - .PinButton(true) - .Dock() - .Resizable() - .FloatingSize(wxDefaultSize) - .BottomDockable(false) - .TopDockable(false) - .BestSize(FromDIP(wxSize(350, 500))) - .MinSize(FromDIP(wxSize(350, -1)))); - - ntbk_cfg_process = - new wxPanel(pn_config, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - wxBoxSizer* bSizer4; - bSizer4 = new wxBoxSizer(wxVERTICAL); - - sld_cfg_pix_filter = - new SliderWithSpin(ntbk_cfg_process, sldid_cfg_PIX_FILTER, wxT("忽略斑点直径"), 8, 0, 50); - bSizer4->Add(sld_cfg_pix_filter, 0, wxEXPAND, 5); - - ntbk_cfg_process->SetSizer(bSizer4); - ntbk_cfg_process->Layout(); - bSizer4->Fit(ntbk_cfg_process); - pn_config->AddPage(ntbk_cfg_process, wxT("处理"), false); - ntbk_cfg_output = - new wxPanel(pn_config, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - wxBoxSizer* bSizer41; - bSizer41 = new wxBoxSizer(wxVERTICAL); - - sld_cfg_border = - new SliderWithSpin(ntbk_cfg_output, sldid_cfg_BORDER, wxT("空白边距"), 10, 0, 100); - bSizer41->Add(sld_cfg_border, 0, wxEXPAND, 5); - - ntbk_cfg_output->SetSizer(bSizer41); - ntbk_cfg_output->Layout(); - bSizer41->Fit(ntbk_cfg_output); - pn_config->AddPage(ntbk_cfg_output, wxT(" 输出"), true); - - m_mgr.Update(); - this->Centre(wxBOTH); -} - -MainUI::~MainUI() { m_mgr.UnInit(); } diff --git a/src/wxUI.h b/src/wxUI.h deleted file mode 100644 index 7d6c84a..0000000 --- a/src/wxUI.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "canvas.h" -#include "ctrl.h" - -namespace Croplines { - -enum { - btnid_CROP_CURR_PAGE = 1000, - btnid_CROP_ALL_PAGE, - - pnid_PAGE_LIST, - - sldid_cfg_PIX_FILTER, - sldid_cfg_BORDER -}; -class MainUI : public wxFrame { - public: - wxAuiToolBar* toolbar; - wxAuiToolBarItem* btn_prev_page; - wxAuiToolBarItem* btn_next_page; - wxAuiToolBarItem* btn_save; - wxAuiToolBarItem* btn_load; - wxAuiToolBarItem* btn_zoom_page; - wxAuiToolBarItem* btn_crop_curr_page; - wxAuiToolBarItem* btn_crop_all_page; - wxStatusBar* status_bar; - wxListBox* pn_page_list; - wxPanel* pn_canvas; - Canvas* canvas; - wxNotebook* pn_config; - wxPanel* ntbk_cfg_process; - SliderWithSpin* sld_cfg_pix_filter; - wxPanel* ntbk_cfg_output; - SliderWithSpin* sld_cfg_border; - - MainUI(wxWindow* parent, wxWindowID id, const wxString& title, - const wxPoint& pos, const wxSize& size, long style); - wxAuiManager m_mgr; - - virtual ~MainUI(); -}; -} // namespace Croplines \ No newline at end of file From 81ce22f258ec02b99361411f0ea6088bf1606b91 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 01:03:24 +0800 Subject: [PATCH 02/16] Update: Usinig `cv::boundingRect` in `CalculateSelectArea` algorithm --- src/core/Page.cpp | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/core/Page.cpp b/src/core/Page.cpp index df5110a..212944e 100644 --- a/src/core/Page.cpp +++ b/src/core/Page.cpp @@ -104,24 +104,19 @@ static std::optional CalculateSelectArea(cv::Mat image, int filter_noise bool has_point = false; for (const auto& contour : contours) { if (cv::contourArea(contour) >= filter_noise_size) { - int x_min_inner = x_min, x_max_inner = x_max, y_min_inner = y_min, y_max_inner = y_max; - for (const auto& point : contour) { - if (point.x == 0 || point.x == image.cols - 1 || point.y == 0 || - point.y == image.rows - 1) { - goto skip_contour; - } - if (point.x < x_min_inner) x_min_inner = point.x; - if (point.x > x_max_inner) x_max_inner = point.x; - if (point.y < y_min_inner) y_min_inner = point.y; - if (point.y > y_max_inner) y_max_inner = point.y; + cv::Rect r = cv::boundingRect(contour); + + // 边缘触碰逻辑(如果靠边则忽略) + if (r.x == 0 || r.y == 0 || (r.x + r.width) >= image.cols || + (r.y + r.height) >= image.rows) { + continue; } - x_min = x_min_inner; - x_max = x_max_inner; - y_min = y_min_inner; - y_max = y_max_inner; + // 更新全局最小/最大边界 + x_min = std::min(x_min, r.x); + y_min = std::min(y_min, r.y); + x_max = std::max(x_max, r.x + r.width); + y_max = std::max(y_max, r.y + r.height); has_point = true; - skip_contour: - (void)0; } } if (!has_point) return std::nullopt; From 96f96d2cf1c77cb047403cf58eb58d721f2f7cb0 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 01:13:25 +0800 Subject: [PATCH 03/16] Style: Rename members --- src/core/Document.cpp | 14 +++---- src/core/Document.hpp | 10 ++--- src/core/Page.cpp | 18 ++++----- src/core/Page.hpp | 14 +++---- src/ui/App.cpp | 4 +- src/ui/App.hpp | 2 +- src/ui/Canvas.cpp | 92 +++++++++++++++++++++--------------------- src/ui/Canvas.hpp | 23 +++++------ src/ui/ConfigPanel.cpp | 4 +- src/ui/Defs.hpp | 6 +-- src/ui/MainFrame.cpp | 48 +++++++++++----------- src/ui/MenuBar.cpp | 64 +++++++++++++++++++---------- src/ui/MenuBar.hpp | 12 ++++-- src/ui/ToolBar.cpp | 20 +++++---- 14 files changed, 178 insertions(+), 153 deletions(-) diff --git a/src/core/Document.cpp b/src/core/Document.cpp index 2507a5a..060a041 100644 --- a/src/core/Document.cpp +++ b/src/core/Document.cpp @@ -29,7 +29,7 @@ bool Document::Load(const fs::path& path) { std::ifstream file(prj_path); if (file) { cereal::JSONInputArchive archive(file); - archive(cereal::make_nvp("prj", getData())); + archive(cereal::make_nvp("prj", GetData())); m_modified = false; return true; } @@ -57,29 +57,29 @@ void Document::InitializeEmptyProject() { m_data->pages, [](std::string_view s1, std::string_view s2) { return NaturalCompare(s1, s2) < 0; }, pred); - setModified(); + SetModified(); } bool Document::Save() { - ASSERT_WITH(isLoad(), "Project not loaded!"); + ASSERT_WITH(IsLoad(), "Project not loaded!"); fs::path prj_path = cwd / PROJECT_FILE_NAME; std::ofstream file(prj_path); cereal::JSONOutputArchive archive(file); - archive(cereal::make_nvp("prj", getData())); + archive(cereal::make_nvp("prj", GetData())); m_modified = false; return true; } bool Document::Close() { - ASSERT_WITH(isLoad(), "Project not loaded!"); + ASSERT_WITH(IsLoad(), "Project not loaded!"); m_modified = false; m_data.reset(); m_processor->ClearCommands(); return true; } -void Document::setModified(bool modified) { m_modified = modified; } +void Document::SetModified(bool modified) { m_modified = modified; } Page Document::LoadPage(size_t index) { ASSERT_WITH(index < PagesSize(), "indx out of range"); @@ -88,7 +88,7 @@ Page Document::LoadPage(size_t index) { } bool Document::SaveAllCrops() { - ASSERT_WITH(isLoad(), "Project not loaded!"); + ASSERT_WITH(IsLoad(), "Project not loaded!"); bool success = true; for (size_t i = 0; i < PagesSize(); i++) { Page page = LoadPage(i); diff --git a/src/core/Document.hpp b/src/core/Document.hpp index 73e79a5..847335f 100644 --- a/src/core/Document.hpp +++ b/src/core/Document.hpp @@ -25,22 +25,22 @@ class Document { bool Load(const std::filesystem::path& path); bool Save(); bool Close(); - bool isLoad() const { return m_data.has_value(); }; + bool IsLoad() const { return m_data.has_value(); }; const std::filesystem::path& GetPath() const { return cwd; } wxCommandProcessor* GetProcessor() const { return m_processor; } - bool isModified() const { return m_modified; } - void setModified(bool modified = true); + bool IsModified() const { return m_modified; } + void SetModified(bool modified = true); size_t PagesSize() const { return m_data->pages.size(); }; Page LoadPage(size_t index); bool SaveAllCrops(); - DocumentData& getData() { return m_data.value(); } - DocumentConfig& getConfig() { return getData().config; } + DocumentData& GetData() { return m_data.value(); } + DocumentConfig& GetConfig() { return GetData().config; } private: bool m_modified = false; diff --git a/src/core/Page.cpp b/src/core/Page.cpp index 212944e..920a84e 100644 --- a/src/core/Page.cpp +++ b/src/core/Page.cpp @@ -12,19 +12,19 @@ using namespace Croplines; namespace fs = std::filesystem; -DocumentConfig& Page::getConfig() const { return m_doc.getConfig(); } +DocumentConfig& Page::GetConfig() const { return m_doc.GetConfig(); } bool Page::InsertLine(int line) { auto [it, modified] = m_pageData.crop_lines.insert(line); m_modified |= modified; - getDocument().setModified(); + GetDocument().SetModified(); return modified; } bool Page::EraseLine(int line) { bool modified = m_pageData.crop_lines.erase(line) != 0; m_modified |= modified; - getDocument().setModified(); + GetDocument().SetModified(); return modified; } @@ -32,7 +32,7 @@ bool Page::SaveCrops() { if (!m_image.IsOk()) return false; std::size_t count = 1; - fs::create_directories(getConfig().output_dir); + fs::create_directories(GetConfig().output_dir); for (wxRect area : getSelectAreas()) { wxImage sub_image = m_image.GetSubImage(area); // TODO @@ -57,8 +57,8 @@ bool Page::SaveCrops() { m_image.GetOptionInt(wxIMAGE_OPTION_TIFF_COMPRESSION)); fs::path file_path = - getConfig().output_dir / std::format("{}-{}{}", getImagePath().stem().string(), count, - getImagePath().extension().string()); + GetConfig().output_dir / std::format("{}-{}{}", GetImagePath().stem().string(), count, + GetImagePath().extension().string()); // bitmap.SaveFile(wxString(file_path), image.GetType()); sub_image.SaveFile(wxString(file_path), m_image.GetType()); count++; @@ -67,7 +67,7 @@ bool Page::SaveCrops() { } std::optional Page::SearchNearestLine(int searchPosition, int threshold) const { - const std::set& cropLines = getCropLines(); + const std::set& cropLines = GetCropLines(); if (cropLines.empty()) return std::nullopt; auto it1 = cropLines.lower_bound(searchPosition); @@ -142,11 +142,11 @@ void Page::CalculateSelectAreas() { auto invokeCalculation = [this, &image](int line, int prev_line) { cv::Mat sub_image = image.rowRange(prev_line, line); - auto area = CalculateSelectArea(sub_image, getConfig().filter_noise_size, 0, prev_line); + auto area = CalculateSelectArea(sub_image, GetConfig().filter_noise_size, 0, prev_line); if (area) m_selectAreas.push_back(*area); }; - for (int line : getCropLines()) { + for (int line : GetCropLines()) { invokeCalculation(line, prev_line); prev_line = line; } diff --git a/src/core/Page.hpp b/src/core/Page.hpp index 270ac14..dc28d45 100644 --- a/src/core/Page.hpp +++ b/src/core/Page.hpp @@ -19,11 +19,11 @@ class Page { public: ~Page() { m_image.Destroy(); } - Document& getDocument() const { return m_doc; } - DocumentConfig& getConfig() const; - const std::filesystem::path& getImagePath() const { return m_pageData.path; } - const std::set& getCropLines() const { return m_pageData.crop_lines; } - wxImage& getImage() { return m_image; } + Document& GetDocument() const { return m_doc; } + DocumentConfig& GetConfig() const; + const std::filesystem::path& GetImagePath() const { return m_pageData.path; } + const std::set& GetCropLines() const { return m_pageData.crop_lines; } + wxImage& GetImage() { return m_image; } const std::vector& getSelectAreas() { if (m_modified) CalculateSelectAreas(); @@ -42,7 +42,7 @@ class Page { Page(Document& doc, PageData& data) : m_doc(doc), m_pageData(data) { LoadImage(); } - bool m_modified = true; // 用来提示 CalculateSelectAreas 的惰性求值 + bool m_modified = true; // 用来提示 CalculateSelectAreas 的惰性求值 Document& m_doc; PageData& m_pageData; @@ -50,7 +50,7 @@ class Page { std::vector m_selectAreas; wxImage m_image; - void LoadImage() { m_image.LoadFile(wxString(getImagePath())); } + void LoadImage() { m_image.LoadFile(wxString(GetImagePath())); } void CalculateSelectAreas(); }; } // namespace Croplines diff --git a/src/ui/App.cpp b/src/ui/App.cpp index b2b67e7..24ba378 100644 --- a/src/ui/App.cpp +++ b/src/ui/App.cpp @@ -16,8 +16,8 @@ bool CroplinesApp::OnInit() { wxImage::AddHandler(new wxJPEGHandler); wxImage::AddHandler(new wxWEBPHandler); - frame = new MainFrame(nullptr, wxID_ANY, wxT("Croplines"), wxDefaultPosition, {900, 600}); - frame->Show(true); + m_frame = new MainFrame(nullptr, wxID_ANY, wxT("Croplines"), wxDefaultPosition, {900, 600}); + m_frame->Show(true); return true; } diff --git a/src/ui/App.hpp b/src/ui/App.hpp index fb8af99..b23a272 100644 --- a/src/ui/App.hpp +++ b/src/ui/App.hpp @@ -6,7 +6,7 @@ namespace Croplines { class CroplinesApp : public wxApp { private: - MainFrame* frame; + MainFrame* m_frame; public: // 这个函数将会在程序启动的时候被调用 diff --git a/src/ui/Canvas.cpp b/src/ui/Canvas.cpp index 426d9b1..8af6791 100644 --- a/src/ui/Canvas.cpp +++ b/src/ui/Canvas.cpp @@ -13,12 +13,12 @@ Canvas::Canvas(wxWindow* parent, wxWindowID id) SetScrollbar(wxHORIZONTAL, -1, -1, -1); SetScrollbar(wxVERTICAL, -1, -1, -1); - context = new wxGLContext(this); + m_glContext = new wxGLContext(this); } Canvas::~Canvas() { - if (texture) glDeleteTextures(1, &texture); - delete context; + if (m_glTexture) glDeleteTextures(1, &m_glTexture); + delete m_glContext; } static void SetTextrue(GLuint texture, void* pixels, int width, int height) { @@ -32,26 +32,26 @@ static void SetTextrue(GLuint texture, void* pixels, int width, int height) { } void Canvas::SetPage(Page& page) { - wxImage img = page.getImage(); + wxImage img = page.GetImage(); if (!img.IsOk()) { return; } if (IsLoaded()) { m_scaleModel.OnImageResize(img.GetSize()); } else { - m_scaleModel = ImageScaleModel{img.GetSize(), this->GetClientSize()}; + m_scaleModel = ImageScaleModel{img.GetSize(), GetClientSize()}; } m_page = &page; - SetCurrent(*context); - if (texture) glDeleteTextures(1, &texture); - glGenTextures(1, &texture); - SetTextrue(texture, img.GetData(), img.GetWidth(), img.GetHeight()); + SetCurrent(*m_glContext); + if (m_glTexture) glDeleteTextures(1, &m_glTexture); + glGenTextures(1, &m_glTexture); + SetTextrue(m_glTexture, img.GetData(), img.GetWidth(), img.GetHeight()); Refresh(); } void Canvas::Clear() { - if (texture) glDeleteTextures(1, &texture); + if (m_glTexture) glDeleteTextures(1, &m_glTexture); m_page = nullptr; Refresh(); } @@ -137,12 +137,12 @@ void UpdateProjection(wxSize size) { void Canvas::OnPaint(wxPaintEvent&) { wxPaintDC dc(this); - SetCurrent(*context); + SetCurrent(*m_glContext); // 初始化OpenGL(首次绘制时执行) - if (!initialized) { + if (!m_isInitialized) { InitGL(); - initialized = true; + m_isInitialized = true; } // 清除缓冲区 @@ -162,7 +162,7 @@ void Canvas::OnPaint(wxPaintEvent&) { wxSize size = m_scaleModel.imageSize; glBegin(GL_QUADS); glColor3d(1.0, 1.0, 1.0); - glBindTexture(GL_TEXTURE_2D, texture); + glBindTexture(GL_TEXTURE_2D, m_glTexture); glTexCoord2d(0, 0); glVertex2d(0, 0); glTexCoord2d(1, 0); @@ -197,8 +197,8 @@ void Canvas::OnPaint(wxPaintEvent&) { // draw crop lines std::optional deleting_line; - if (is_deleting && mouse_position) { - int y = mouse_position->y; + if (m_isDeleting && m_mouseCurrentPosition) { + int y = m_mouseCurrentPosition->y; y = std::lround(m_scaleModel.ReverseTransformY(y)); auto search_line = m_page->SearchNearestLine( @@ -211,18 +211,18 @@ void Canvas::OnPaint(wxPaintEvent&) { } glColor4d(0.30, 0.74, 0.52, 0.5); - for (int line : m_page->getCropLines()) { + for (int line : m_page->GetCropLines()) { if (deleting_line == line) continue; DrawLine(2, line); } // draw mouse line - if (mouse_position) { - if (is_deleting) + if (m_mouseCurrentPosition) { + if (m_isDeleting) glColor4d(0.90, 0.08, 0, 0.5); else glColor4d(0.34, 0.61, 0.84, 0.5); - double y = m_scaleModel.ReverseTransformY(mouse_position->y); + double y = m_scaleModel.ReverseTransformY(m_mouseCurrentPosition->y); DrawLine(2, y); } } @@ -231,8 +231,8 @@ void Canvas::OnPaint(wxPaintEvent&) { } void Canvas::OnSize(wxSizeEvent& event) { - if (context) { - SetCurrent(*context); + if (m_glContext) { + SetCurrent(*m_glContext); UpdateProjection(GetClientSize()); } if (IsLoaded()) m_scaleModel.OnWindowResize(event.GetSize()); @@ -255,33 +255,33 @@ void Canvas::OnMouseWheel(wxMouseEvent& event) { void Canvas::OnMouseLeftDown(wxMouseEvent& event) { if (!IsLoaded()) return; - if (!is_mouse_capture) { + if (!m_isMouseCaptured) { CaptureMouse(); - is_mouse_capture = true; + m_isMouseCaptured = true; } // m_parent->GetParent()->SetFocus(); SetFocus(); - mouse_drag_start = event.GetPosition(); + m_mouseDragStartPosition = event.GetPosition(); } void Canvas::OnMouseLeftUp(wxMouseEvent&) { if (!IsLoaded()) return; - if (is_mouse_capture) { + if (m_isMouseCaptured) { ReleaseMouse(); - is_mouse_capture = false; + m_isMouseCaptured = false; } - mouse_drag_start.reset(); + m_mouseDragStartPosition.reset(); } void Canvas::OnMouseLeftUp(wxMouseCaptureLostEvent&) { if (!IsLoaded()) return; - if (is_mouse_capture) { + if (m_isMouseCaptured) { ReleaseMouse(); - is_mouse_capture = false; + m_isMouseCaptured = false; } - mouse_drag_start.reset(); + m_mouseDragStartPosition.reset(); } void Canvas::OnMouseRightDown(wxMouseEvent& event) { @@ -317,7 +317,7 @@ void Canvas::OnMouseRightUp(wxMouseEvent& event) { int y = mousePosition.y; y = std::lround(m_scaleModel.ReverseTransformY(y)); - if (is_deleting) { + if (m_isDeleting) { int threshold = static_cast(5.0 / m_scaleModel.scale) + 1; auto line = m_page->SearchNearestLine(y, FromDIP(threshold)); if (line.has_value()) GetProcessor()->Submit(new EraseLineCommand{GetPage(), *line}); @@ -331,20 +331,20 @@ void Canvas::OnMouseMotion(wxMouseEvent& event) { if (!IsLoaded()) return; wxPoint mouse_drag = event.GetPosition(); - if (mouse_drag_start && event.Dragging()) { - auto dr = mouse_drag - *mouse_drag_start; + if (m_mouseDragStartPosition && event.Dragging()) { + auto dr = mouse_drag - *m_mouseDragStartPosition; m_scaleModel.Move(dr); - *mouse_drag_start = mouse_drag; + *m_mouseDragStartPosition = mouse_drag; Refresh(); } // if mouse inside image wxPoint mouse_position = event.GetPosition(); if (m_scaleModel.IsInsideImage(mouse_position)) { - this->mouse_position = mouse_position; + m_mouseCurrentPosition = mouse_position; Refresh(); - } else if (this->mouse_position) { - this->mouse_position = std::nullopt; + } else if (m_mouseCurrentPosition) { + m_mouseCurrentPosition = std::nullopt; Refresh(); } } @@ -378,8 +378,8 @@ void Canvas::OnKeyUp(wxKeyEvent& event) { switch (event.GetKeyCode()) { case 'D': - if (is_deleting) { - is_deleting = false; + if (m_isDeleting) { + m_isDeleting = false; Refresh(); } break; @@ -391,8 +391,8 @@ void Canvas::OnKeyDown(wxKeyEvent& event) { switch (event.GetKeyCode()) { case 'D': - if (!is_deleting) { - is_deleting = true; + if (!m_isDeleting) { + m_isDeleting = true; Refresh(); } break; @@ -402,13 +402,13 @@ void Canvas::OnKeyDown(wxKeyEvent& event) { void Canvas::OnKillFocus(wxFocusEvent& event) { if (!IsLoaded()) return; - if (is_deleting) { - is_deleting = false; + if (m_isDeleting) { + m_isDeleting = false; Refresh(); } - if (is_mouse_capture) { + if (m_isMouseCaptured) { ReleaseMouse(); - is_mouse_capture = false; + m_isMouseCaptured = false; } event.Skip(); } diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp index 88264be..857bfca 100644 --- a/src/ui/Canvas.hpp +++ b/src/ui/Canvas.hpp @@ -17,7 +17,7 @@ class Canvas : public wxGLCanvas { void SetPage(Page& page); Page& GetPage() { return *m_page; } - Document& GetDocument() { return GetPage().getDocument(); } + Document& GetDocument() { return GetPage().GetDocument(); } wxCommandProcessor* GetProcessor() { return GetDocument().GetProcessor(); } void Clear(); @@ -30,23 +30,20 @@ class Canvas : public wxGLCanvas { void Zoom(double scale); private: - bool is_deleting = false; - bool initialized = false; + bool m_isDeleting = false; + bool m_isInitialized = false; - bool is_mouse_capture = false; - std::optional mouse_drag_start; - std::optional mouse_position; - - wxBitmap drawBmp; + bool m_isMouseCaptured = false; + std::optional m_mouseDragStartPosition; + std::optional m_mouseCurrentPosition; Page* m_page = nullptr; ImageScaleModel m_scaleModel; - wxGLContext* context; - GLuint texture; - cv::UMat uimageSrc; - wxImage imageDst; - bool imageModified = false; + wxGLContext* m_glContext; + GLuint m_glTexture; + wxImage m_imageDst; + bool m_isImageModified = false; private: void UpdateScrollbars(); diff --git a/src/ui/ConfigPanel.cpp b/src/ui/ConfigPanel.cpp index c8cbd45..a213562 100644 --- a/src/ui/ConfigPanel.cpp +++ b/src/ui/ConfigPanel.cpp @@ -23,7 +23,7 @@ ConfigPanel::ConfigPanel(wxWindow* parent, wxWindowID id) : wxNotebook(parent, i ProcessConfigPage::ProcessConfigPage(wxWindow* parent, wxWindowID id) : wxPanel(parent, id) { m_sliderPixFilter = - new SliderWithSpin{this, sldid_cfg_PIX_FILTER, wxT("忽略斑点直径"), 8, 0, 50}; + new SliderWithSpin{this, sliderID_cfg_PIX_FILTER, wxT("忽略斑点直径"), 8, 0, 50}; auto* bSizer = new wxBoxSizer{wxVERTICAL}; bSizer->Add(m_sliderPixFilter, 0, wxEXPAND, 5); @@ -34,7 +34,7 @@ ProcessConfigPage::ProcessConfigPage(wxWindow* parent, wxWindowID id) : wxPanel( } OutputConfigPage::OutputConfigPage(wxWindow* parent, wxWindowID id) : wxPanel(parent, id) { - m_sliderBorder = new SliderWithSpin{this, sldid_cfg_BORDER, wxT("空白边距"), 10, 0, 100}; + m_sliderBorder = new SliderWithSpin{this, sliderID_cfg_BORDER, wxT("空白边距"), 10, 0, 100}; auto* bSizer = new wxBoxSizer{wxVERTICAL}; bSizer->Add(m_sliderBorder, 0, wxEXPAND, 5); diff --git a/src/ui/Defs.hpp b/src/ui/Defs.hpp index 27c1b75..05a6293 100644 --- a/src/ui/Defs.hpp +++ b/src/ui/Defs.hpp @@ -5,9 +5,9 @@ enum { buttonID_CROP_CURR_PAGE = 1000, buttonID_CROP_ALL_PAGE, - pnid_PAGE_LIST, + panelID_PAGE_LIST, - sldid_cfg_PIX_FILTER, - sldid_cfg_BORDER + sliderID_cfg_PIX_FILTER, + sliderID_cfg_BORDER }; } diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp index 18614fd..a7b0f56 100644 --- a/src/ui/MainFrame.cpp +++ b/src/ui/MainFrame.cpp @@ -81,7 +81,7 @@ MainFrame::MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, con .BestSize(FromDIP(wxSize(350, 500))) .MinSize(FromDIP(wxSize(350, -1)))); - m_pageListPanel = new wxListBox{this, pnid_PAGE_LIST, wxDefaultPosition, wxDefaultSize, + m_pageListPanel = new wxListBox{this, panelID_PAGE_LIST, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_NEEDED_SB}; m_mgr.AddPane(m_pageListPanel, wxAuiPaneInfo() .Left() @@ -104,7 +104,7 @@ MainFrame::MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, con } bool MainFrame::Load(std::filesystem::path path) { - if (m_doc.isLoad()) { + if (m_doc.IsLoad()) { if (!Close()) return false; } @@ -136,7 +136,7 @@ static bool ShowCloseDialog(wxWindow* parent, Document& doc) { } bool MainFrame::Save() { - if (m_doc.isLoad()) { + if (m_doc.IsLoad()) { m_doc.Save(); return true; } else @@ -144,8 +144,8 @@ bool MainFrame::Save() { } bool MainFrame::Close() { - if (m_doc.isLoad()) { - if (m_doc.isModified()) { + if (m_doc.IsLoad()) { + if (m_doc.IsModified()) { if (!ShowCloseDialog(this, m_doc)) return false; } SetFocus(); // 防止焦点在需要禁用的组件中 @@ -160,7 +160,7 @@ bool MainFrame::Close() { } void MainFrame::CurrentPage(std::size_t page) { - if (m_doc.isLoad()) { + if (m_doc.IsLoad()) { m_currentPageIdx = std::clamp(page, static_cast(0), m_doc.PagesSize() - 1); m_toolBar->EnableTool(wxID_UP, m_currentPageIdx != 0); m_toolBar->EnableTool(wxID_DOWN, m_currentPageIdx != m_doc.PagesSize() - 1); @@ -173,21 +173,21 @@ void MainFrame::CurrentPage(std::size_t page) { void MainFrame::SyncUIPageListPanel() { m_pageListPanel->Clear(); - if (m_doc.isLoad() && m_doc.PagesSize() != 0) { + if (m_doc.IsLoad() && m_doc.PagesSize() != 0) { std::vector file_names; file_names.reserve(m_doc.PagesSize()); std::ranges::transform( - m_doc.getData().pages, std::back_inserter(file_names), + m_doc.GetData().pages, std::back_inserter(file_names), [](const std::unique_ptr& page) { return wxString(page->path.filename()); }); m_pageListPanel->Set(file_names); } } -void MainFrame::SyncUIConfigPanel() { m_configPanel->SyncUI(m_doc.getConfig()); } +void MainFrame::SyncUIConfigPanel() { m_configPanel->SyncUI(m_doc.GetConfig()); } void MainFrame::OnLoad(wxCommandEvent&) { - if (m_doc.isLoad() && m_doc.isModified()) { + if (m_doc.IsLoad() && m_doc.IsModified()) { if (!ShowCloseDialog(this, m_doc)) return; } wxDirDialog dir_dialog(this, wxT("选择目录"), wxEmptyString, @@ -200,14 +200,14 @@ void MainFrame::OnLoad(wxCommandEvent&) { } void MainFrame::OnUndo(wxCommandEvent&) { - if (m_doc.isLoad()) { + if (m_doc.IsLoad()) { m_doc.GetProcessor()->Undo(); m_canvas->Refresh(); } } void MainFrame::OnRedo(wxCommandEvent&) { - if (m_doc.isLoad()) { + if (m_doc.IsLoad()) { m_doc.GetProcessor()->Redo(); m_canvas->Refresh(); } @@ -225,7 +225,7 @@ void MainFrame::OnCropCurrPage(wxCommandEvent&) { } void MainFrame::OnCropAllPage(wxCommandEvent&) { - if (!m_doc.isLoad()) return; + if (!m_doc.IsLoad()) return; if (m_canvas->IsLoaded()) { // std::thread t([this]() { @@ -284,7 +284,7 @@ class ChangeConfigCommand : public wxCommand { << m_newValue << std::endl; if (m_slider && m_slider->GetValue() != m_newValue) m_slider->SetValue(m_newValue); m_placeToModify = m_newValue; - m_doc.setModified(); + m_doc.SetModified(); return true; } @@ -293,7 +293,7 @@ class ChangeConfigCommand : public wxCommand { << m_oldValue << std::endl; if (m_slider && m_slider->GetValue() != m_oldValue) m_slider->SetValue(m_oldValue); m_placeToModify = m_oldValue; - m_doc.setModified(); + m_doc.SetModified(); return true; } @@ -307,18 +307,18 @@ class ChangeConfigCommand : public wxCommand { }; void MainFrame::OnChnageCfgFilerPixSize(wxCommandEvent& event) { - wxWindow* win = FindWindowById(sldid_cfg_PIX_FILTER); + wxWindow* win = FindWindowById(sliderID_cfg_PIX_FILTER); SliderWithSpin* slider = wxDynamicCast(win, SliderWithSpin); - auto* command = new ChangeConfigCommand{slider, m_doc, m_doc.getConfig().filter_noise_size, + auto* command = new ChangeConfigCommand{slider, m_doc, m_doc.GetConfig().filter_noise_size, event.GetInt()}; m_doc.GetProcessor()->Submit(command); } void MainFrame::OnChangeCfgBorder(wxCommandEvent& event) { - wxWindow* win = FindWindowById(sldid_cfg_BORDER); + wxWindow* win = FindWindowById(sliderID_cfg_BORDER); SliderWithSpin* slider = wxDynamicCast(win, SliderWithSpin); auto* command = - new ChangeConfigCommand{slider, m_doc, m_doc.getConfig().border, event.GetInt()}; + new ChangeConfigCommand{slider, m_doc, m_doc.GetConfig().border, event.GetInt()}; m_doc.GetProcessor()->Submit(command); } @@ -329,8 +329,8 @@ void MainFrame::OnUpdateRedo(wxUpdateUIEvent&) { m_menuBar->Enable(wxID_REDO, m_doc.GetProcessor()->CanRedo()); } void MainFrame::OnUpdateSave(wxUpdateUIEvent&) { - m_menuBar->Enable(wxID_SAVE, m_doc.isModified()); - m_toolBar->EnableTool(wxID_SAVE, m_doc.isModified()); + m_menuBar->Enable(wxID_SAVE, m_doc.IsModified()); + m_toolBar->EnableTool(wxID_SAVE, m_doc.IsModified()); m_toolBar->Refresh(); } @@ -352,9 +352,9 @@ wxBEGIN_EVENT_TABLE(MainFrame, wxFrame) EVT_MENU(wxID_EXIT, MainFrame::OnExit) EVT_MENU(wxID_ABOUT, MainFrame::OnAbout) EVT_CLOSE(MainFrame::OnExit) - EVT_LISTBOX(pnid_PAGE_LIST, MainFrame::OnClickListBox) - EVT_SLIDER(sldid_cfg_PIX_FILTER, MainFrame::OnChnageCfgFilerPixSize) - EVT_SLIDER(sldid_cfg_BORDER, MainFrame::OnChangeCfgBorder) + EVT_LISTBOX(panelID_PAGE_LIST, MainFrame::OnClickListBox) + EVT_SLIDER(sliderID_cfg_PIX_FILTER, MainFrame::OnChnageCfgFilerPixSize) + EVT_SLIDER(sliderID_cfg_BORDER, MainFrame::OnChangeCfgBorder) // Update ui EVT_UPDATE_UI(wxID_UNDO, MainFrame::OnUpdateUndo) diff --git a/src/ui/MenuBar.cpp b/src/ui/MenuBar.cpp index f46e348..6659c59 100644 --- a/src/ui/MenuBar.cpp +++ b/src/ui/MenuBar.cpp @@ -5,37 +5,57 @@ using namespace Croplines; MenuBar::MenuBar() : wxMenuBar() { - menu_file->Append(wxID_OPEN, wxT("&Load\tCtrl+O")); - menu_file->Append(wxID_SAVE); - menu_file->AppendSeparator(); - menu_file->Append(buttonID_CROP_CURR_PAGE, wxT("Crop ¤t page"), + m_menuFile->Append(wxID_OPEN, wxT("&Load\tCtrl+O")); + m_menuFile->Append(wxID_SAVE); + m_menuFile->AppendSeparator(); + m_menuFile->Append(buttonID_CROP_CURR_PAGE, wxT("Crop ¤t page"), wxT("Crop current page to subimages and save each one to " "output directory")); - menu_file->Append(buttonID_CROP_ALL_PAGE, wxT("Crop &all pages"), + m_menuFile->Append(buttonID_CROP_ALL_PAGE, wxT("Crop &all pages"), wxT("Crop all pages to subimages and save each one to " "output directory")); - menu_file->AppendSeparator(); - menu_file->Append(wxID_CLOSE); - menu_file->Append(wxID_EXIT); + m_menuFile->AppendSeparator(); + m_menuFile->Append(wxID_CLOSE); + m_menuFile->Append(wxID_EXIT); - Append(menu_file, wxT("&File")); + Append(m_menuFile, wxT("&File")); - menu_edit->Append(wxID_UNDO); - menu_edit->Append(wxID_REDO); - menu_edit->AppendSeparator(); - menu_edit->Append(wxID_UP, wxT("Last page\tUp"), wxT("Move to last page")); - menu_edit->Append(wxID_DOWN, wxT("Next page\tDown"), wxT("Move to next page")); + m_menuEdit->Append(wxID_UNDO); + m_menuEdit->Append(wxID_REDO); + m_menuEdit->AppendSeparator(); + m_menuEdit->Append(wxID_UP, wxT("Last page\tUp"), wxT("Move to last page")); + m_menuEdit->Append(wxID_DOWN, wxT("Next page\tDown"), wxT("Move to next page")); - Append(menu_edit, wxT("&Edit")); + Append(m_menuEdit, wxT("&Edit")); - menu_view->Append(wxID_ZOOM_IN); - menu_view->Append(wxID_ZOOM_OUT); - menu_view->Append(wxID_ZOOM_FIT); - menu_view->Append(wxID_ZOOM_100); + m_menuView->Append(wxID_ZOOM_IN); + m_menuView->Append(wxID_ZOOM_OUT); + m_menuView->Append(wxID_ZOOM_FIT); + m_menuView->Append(wxID_ZOOM_100); - Append(menu_view, wxT("&View")); + Append(m_menuView, wxT("&View")); - menu_help->Append(wxID_ABOUT); + m_menuHelp->Append(wxID_ABOUT); - Append(menu_help, wxT("&Help")); + Append(m_menuHelp, wxT("&Help")); +} + +bool MenuBar::Enable(bool enable) { + // Do not enable wxID_OPEN + static int needToEnable[] = { + wxID_SAVE, + wxID_CLOSE, + wxID_UP, + wxID_DOWN, + wxID_ZOOM_FIT, + wxID_ZOOM_OUT, + wxID_ZOOM_IN, + wxID_ZOOM_100, + buttonID_CROP_ALL_PAGE, + buttonID_CROP_CURR_PAGE, + }; + + for (int id : needToEnable) Enable(id, enable); + + return true; } diff --git a/src/ui/MenuBar.hpp b/src/ui/MenuBar.hpp index 94a51c0..cc5051c 100644 --- a/src/ui/MenuBar.hpp +++ b/src/ui/MenuBar.hpp @@ -1,15 +1,19 @@ #pragma once #include +#include namespace Croplines { class MenuBar : public wxMenuBar { public: - wxMenu* menu_file = new wxMenu{}; - wxMenu* menu_edit = new wxMenu{}; - wxMenu* menu_view = new wxMenu{}; - wxMenu* menu_help = new wxMenu{}; + wxMenu* m_menuFile = new wxMenu{}; + wxMenu* m_menuEdit = new wxMenu{}; + wxMenu* m_menuView = new wxMenu{}; + wxMenu* m_menuHelp = new wxMenu{}; MenuBar(); + + using wxMenuBar::Enable; + bool Enable(bool enable = true) override; }; } // namespace Croplines diff --git a/src/ui/ToolBar.cpp b/src/ui/ToolBar.cpp index 9b50370..2f0c3b1 100644 --- a/src/ui/ToolBar.cpp +++ b/src/ui/ToolBar.cpp @@ -36,14 +36,18 @@ ToolBar::ToolBar(wxWindow* parent, wxWindowID id) } bool ToolBar::Enable(bool state) { - EnableTool(wxID_UP, state); - EnableTool(wxID_DOWN, state); - EnableTool(wxID_SAVE, state); - // EnableTool(wxID_OPEN, state); - EnableTool(wxID_ZOOM_FIT, state); - EnableTool(buttonID_CROP_CURR_PAGE, state); - EnableTool(buttonID_CROP_ALL_PAGE, state); - Refresh(); + // Do not enable wxID_OPEN + static int needToEnable[] = { + wxID_UP, + wxID_DOWN, + wxID_SAVE, + wxID_ZOOM_FIT, + buttonID_CROP_ALL_PAGE, + buttonID_CROP_CURR_PAGE, + }; + + for (int id : needToEnable) EnableTool(id, state); + Refresh(); return true; } From b288a803df18501457e23bc7cf8f5c30f2f16375 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 01:18:03 +0800 Subject: [PATCH 04/16] Style: Rename namespace to lower case --- src/core/Document.cpp | 2 +- src/core/Document.hpp | 4 ++-- src/core/DocumentData.hpp | 4 ++-- src/core/Event.hpp | 4 ++-- src/core/ImageScaleModel.cpp | 2 +- src/core/ImageScaleModel.hpp | 4 ++-- src/core/Page.cpp | 2 +- src/core/Page.hpp | 4 ++-- src/ui/App.cpp | 2 +- src/ui/App.hpp | 4 ++-- src/ui/Canvas.cpp | 2 +- src/ui/Canvas.hpp | 4 ++-- src/ui/ConfigPanel.cpp | 2 +- src/ui/ConfigPanel.hpp | 4 ++-- src/ui/Defs.hpp | 2 +- src/ui/MainFrame.cpp | 2 +- src/ui/MainFrame.hpp | 4 ++-- src/ui/MenuBar.cpp | 2 +- src/ui/MenuBar.hpp | 4 ++-- src/ui/ToolBar.cpp | 2 +- src/ui/ToolBar.hpp | 4 ++-- src/ui/components/SliderWithSpin.cpp | 2 +- src/ui/components/SliderWithSpin.hpp | 4 ++-- src/utils/Compare.hpp | 2 +- 24 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/core/Document.cpp b/src/core/Document.cpp index 060a041..9d908c0 100644 --- a/src/core/Document.cpp +++ b/src/core/Document.cpp @@ -13,7 +13,7 @@ #include "utils/Asserts.hpp" #include "utils/Compare.hpp" -using namespace Croplines; +using namespace croplines; namespace fs = std::filesystem; bool Document::Load(const fs::path& path) { diff --git a/src/core/Document.hpp b/src/core/Document.hpp index 847335f..ac21eb3 100644 --- a/src/core/Document.hpp +++ b/src/core/Document.hpp @@ -14,7 +14,7 @@ #include "core/Event.hpp" #include "core/Page.hpp" -namespace Croplines { +namespace croplines { class Document { public: @@ -54,4 +54,4 @@ class Document { void InitializeEmptyProject(); }; -} // namespace Croplines +} // namespace croplines diff --git a/src/core/DocumentData.hpp b/src/core/DocumentData.hpp index 2c4fdf0..f362bc0 100644 --- a/src/core/DocumentData.hpp +++ b/src/core/DocumentData.hpp @@ -10,7 +10,7 @@ #include #include -namespace Croplines { +namespace croplines { struct DocumentConfig { constexpr static const char* DEFAULT_OUTPUT_DIR = "out"; @@ -76,7 +76,7 @@ struct DocumentData { ar(cereal::make_nvp("pages", PagesProxy{pages})); } }; -} // namespace Croplines +} // namespace croplines // Add support for cereal serializing std::filesystem::path namespace std { diff --git a/src/core/Event.hpp b/src/core/Event.hpp index 50ef5d2..edf62f4 100644 --- a/src/core/Event.hpp +++ b/src/core/Event.hpp @@ -1,7 +1,7 @@ #include #include -namespace Croplines { +namespace croplines { wxDECLARE_EVENT(EVT_DOCUMENT_CHANGED, wxCommandEvent); @@ -20,4 +20,4 @@ class DocumentEvent final : public wxCommandEvent { Type m_type; }; -} // namespace Croplines +} // namespace croplines diff --git a/src/core/ImageScaleModel.cpp b/src/core/ImageScaleModel.cpp index 1c71a13..6e133f3 100644 --- a/src/core/ImageScaleModel.cpp +++ b/src/core/ImageScaleModel.cpp @@ -1,6 +1,6 @@ #include "core/ImageScaleModel.hpp" -using namespace Croplines; +using namespace croplines; ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale) : imageSize(imageSize), windowSize(windowSize), scale(scale) { diff --git a/src/core/ImageScaleModel.hpp b/src/core/ImageScaleModel.hpp index d4cf531..cfd88be 100644 --- a/src/core/ImageScaleModel.hpp +++ b/src/core/ImageScaleModel.hpp @@ -2,7 +2,7 @@ #include #include -namespace Croplines { +namespace croplines { class ImageScaleModel { public: @@ -53,4 +53,4 @@ class ImageScaleModel { private: void Clamp(); }; -} // namespace Croplines +} // namespace croplines diff --git a/src/core/Page.cpp b/src/core/Page.cpp index 920a84e..e50b28e 100644 --- a/src/core/Page.cpp +++ b/src/core/Page.cpp @@ -9,7 +9,7 @@ #include "core/DocumentData.hpp" #include "utils/Asserts.hpp" -using namespace Croplines; +using namespace croplines; namespace fs = std::filesystem; DocumentConfig& Page::GetConfig() const { return m_doc.GetConfig(); } diff --git a/src/core/Page.hpp b/src/core/Page.hpp index dc28d45..a93ef13 100644 --- a/src/core/Page.hpp +++ b/src/core/Page.hpp @@ -11,7 +11,7 @@ #include "core/DocumentData.hpp" -namespace Croplines { +namespace croplines { class Document; @@ -53,4 +53,4 @@ class Page { void LoadImage() { m_image.LoadFile(wxString(GetImagePath())); } void CalculateSelectAreas(); }; -} // namespace Croplines +} // namespace croplines diff --git a/src/ui/App.cpp b/src/ui/App.cpp index 24ba378..e60b414 100644 --- a/src/ui/App.cpp +++ b/src/ui/App.cpp @@ -5,7 +5,7 @@ #include "ui/MainFrame.hpp" -using namespace Croplines; +using namespace croplines; IMPLEMENT_APP(CroplinesApp) diff --git a/src/ui/App.hpp b/src/ui/App.hpp index b23a272..a482e3f 100644 --- a/src/ui/App.hpp +++ b/src/ui/App.hpp @@ -3,7 +3,7 @@ #include "ui/MainFrame.hpp" -namespace Croplines { +namespace croplines { class CroplinesApp : public wxApp { private: MainFrame* m_frame; @@ -12,4 +12,4 @@ class CroplinesApp : public wxApp { // 这个函数将会在程序启动的时候被调用 bool OnInit() override; }; -} // namespace Croplines +} // namespace croplines diff --git a/src/ui/Canvas.cpp b/src/ui/Canvas.cpp index 8af6791..6b9d6a4 100644 --- a/src/ui/Canvas.cpp +++ b/src/ui/Canvas.cpp @@ -2,7 +2,7 @@ #include -using namespace Croplines; +using namespace croplines; Canvas::Canvas(wxWindow* parent, wxWindowID id) : wxGLCanvas(parent, id, nullptr, wxDefaultPosition, wxDefaultSize, diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp index 857bfca..28a2f7f 100644 --- a/src/ui/Canvas.hpp +++ b/src/ui/Canvas.hpp @@ -9,7 +9,7 @@ #include "core/ImageScaleModel.hpp" #include "core/Page.hpp" -namespace Croplines { +namespace croplines { class Canvas : public wxGLCanvas { public: Canvas(wxWindow* parent, wxWindowID id); @@ -68,4 +68,4 @@ class Canvas : public wxGLCanvas { wxDECLARE_EVENT_TABLE(); }; -} // namespace Croplines +} // namespace croplines diff --git a/src/ui/ConfigPanel.cpp b/src/ui/ConfigPanel.cpp index a213562..ef88021 100644 --- a/src/ui/ConfigPanel.cpp +++ b/src/ui/ConfigPanel.cpp @@ -11,7 +11,7 @@ #include "core/DocumentData.hpp" #include "ui/Defs.hpp" -using namespace Croplines; +using namespace croplines; ConfigPanel::ConfigPanel(wxWindow* parent, wxWindowID id) : wxNotebook(parent, id) { m_processPage = new ProcessConfigPage{this, wxID_ANY}; diff --git a/src/ui/ConfigPanel.hpp b/src/ui/ConfigPanel.hpp index e262f65..75d7270 100644 --- a/src/ui/ConfigPanel.hpp +++ b/src/ui/ConfigPanel.hpp @@ -8,7 +8,7 @@ #include "core/DocumentData.hpp" #include "ui/components/SliderWithSpin.hpp" -namespace Croplines { +namespace croplines { class ProcessConfigPage; class OutputConfigPage; @@ -44,4 +44,4 @@ class OutputConfigPage : public wxPanel { SliderWithSpin* m_sliderBorder; }; -} // namespace Croplines +} // namespace croplines diff --git a/src/ui/Defs.hpp b/src/ui/Defs.hpp index 05a6293..3993d93 100644 --- a/src/ui/Defs.hpp +++ b/src/ui/Defs.hpp @@ -1,6 +1,6 @@ #pragma once -namespace Croplines { +namespace croplines { enum { buttonID_CROP_CURR_PAGE = 1000, buttonID_CROP_ALL_PAGE, diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp index a7b0f56..d504c13 100644 --- a/src/ui/MainFrame.cpp +++ b/src/ui/MainFrame.cpp @@ -22,7 +22,7 @@ #include "ui/ToolBar.hpp" #include "utils/Asserts.hpp" -using namespace Croplines; +using namespace croplines; MainFrame::MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size) diff --git a/src/ui/MainFrame.hpp b/src/ui/MainFrame.hpp index 45368db..eabe239 100644 --- a/src/ui/MainFrame.hpp +++ b/src/ui/MainFrame.hpp @@ -15,7 +15,7 @@ #include "ui/MenuBar.hpp" #include "ui/ToolBar.hpp" -namespace Croplines { +namespace croplines { class MainFrame final : public wxFrame { public: MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, @@ -75,4 +75,4 @@ class MainFrame final : public wxFrame { wxDECLARE_EVENT_TABLE(); }; -} // namespace Croplines +} // namespace croplines diff --git a/src/ui/MenuBar.cpp b/src/ui/MenuBar.cpp index 6659c59..2a8e983 100644 --- a/src/ui/MenuBar.cpp +++ b/src/ui/MenuBar.cpp @@ -2,7 +2,7 @@ #include "ui/Defs.hpp" -using namespace Croplines; +using namespace croplines; MenuBar::MenuBar() : wxMenuBar() { m_menuFile->Append(wxID_OPEN, wxT("&Load\tCtrl+O")); diff --git a/src/ui/MenuBar.hpp b/src/ui/MenuBar.hpp index cc5051c..a76fa6b 100644 --- a/src/ui/MenuBar.hpp +++ b/src/ui/MenuBar.hpp @@ -3,7 +3,7 @@ #include #include -namespace Croplines { +namespace croplines { class MenuBar : public wxMenuBar { public: wxMenu* m_menuFile = new wxMenu{}; @@ -16,4 +16,4 @@ class MenuBar : public wxMenuBar { using wxMenuBar::Enable; bool Enable(bool enable = true) override; }; -} // namespace Croplines +} // namespace croplines diff --git a/src/ui/ToolBar.cpp b/src/ui/ToolBar.cpp index 2f0c3b1..4a3f7ea 100644 --- a/src/ui/ToolBar.cpp +++ b/src/ui/ToolBar.cpp @@ -4,7 +4,7 @@ #include "ui/Defs.hpp" -using namespace Croplines; +using namespace croplines; ToolBar::ToolBar(wxWindow* parent, wxWindowID id) : wxAuiToolBar(parent, id, wxDefaultPosition, wxDefaultSize, diff --git a/src/ui/ToolBar.hpp b/src/ui/ToolBar.hpp index fcef77a..67d558c 100644 --- a/src/ui/ToolBar.hpp +++ b/src/ui/ToolBar.hpp @@ -2,11 +2,11 @@ #include #include -namespace Croplines { +namespace croplines { class ToolBar final : public wxAuiToolBar { public: ToolBar(wxWindow* parent, wxWindowID id); bool Enable(bool state = true) override; }; -} // namespace Croplines +} // namespace croplines diff --git a/src/ui/components/SliderWithSpin.cpp b/src/ui/components/SliderWithSpin.cpp index 4b23743..e8b7a69 100644 --- a/src/ui/components/SliderWithSpin.cpp +++ b/src/ui/components/SliderWithSpin.cpp @@ -3,7 +3,7 @@ #include #include -using namespace Croplines; +using namespace croplines; constexpr int SLIDER_ID = 1100; constexpr int SPIN_ID = 1101; diff --git a/src/ui/components/SliderWithSpin.hpp b/src/ui/components/SliderWithSpin.hpp index e636501..e64cbcf 100644 --- a/src/ui/components/SliderWithSpin.hpp +++ b/src/ui/components/SliderWithSpin.hpp @@ -6,7 +6,7 @@ #include #include -namespace Croplines { +namespace croplines { class SliderWithSpin : public wxPanel { public: SliderWithSpin() : wxPanel() {} @@ -34,4 +34,4 @@ class SliderWithSpin : public wxPanel { wxDECLARE_EVENT_TABLE(); wxDECLARE_DYNAMIC_CLASS_NO_COPY(SliderWithSpin); }; -} // namespace Croplines +} // namespace croplines diff --git a/src/utils/Compare.hpp b/src/utils/Compare.hpp index cdf947b..eab39eb 100644 --- a/src/utils/Compare.hpp +++ b/src/utils/Compare.hpp @@ -1,6 +1,6 @@ #include #include -namespace Croplines { +namespace croplines { std::strong_ordering NaturalCompare(std::string_view a, std::string_view b); } From a25095cef2a9147ddb64d31c9e1648cfd4b5297b Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 01:49:57 +0800 Subject: [PATCH 05/16] Update: Impose strict clang tidy rules. --- .clang-tidy | 36 ++++++++++++++++-- src/core/Document.hpp | 20 +++++----- src/core/Event.hpp | 4 +- src/core/ImageScaleModel.cpp | 3 ++ src/core/ImageScaleModel.hpp | 23 ++++++----- src/core/Page.cpp | 2 +- src/ui/Canvas.cpp | 57 ++++++++++++++-------------- src/ui/Canvas.hpp | 15 +++++--- src/ui/Defs.hpp | 2 +- src/ui/MainFrame.cpp | 4 +- src/ui/MainFrame.hpp | 10 ++--- src/ui/components/SliderWithSpin.hpp | 12 +++--- 12 files changed, 112 insertions(+), 76 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index f7b7ce1..c6f147e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -47,16 +47,24 @@ Checks: bugprone-exception-escape, bugprone-missing-return, + performance-avoid-endl, performance-faster-string-find, performance-for-range-copy, performance-implicit-conversion-in-loop, performance-inefficient-algorithm, + performance-inefficient-string-concatenation, performance-inefficient-vector-operation, + performance-move-const-arg, performance-move-constructor-init, performance-no-automatic-move, + performance-no-int-to-ptr, + performance-noexcept-destructor, + performance-noexcept-move-constructor, + performance-noexcept-swap, performance-trivially-destructible, + performance-type-promotion-in-math-fn, performance-unnecessary-copy-initialization, - performance-move-const-arg, + performance-unnecessary-value-param, modernize-avoid-bind, modernize-loop-convert, @@ -68,11 +76,27 @@ Checks: modernize-replace-random-shuffle, modernize-use-auto, modernize-use-bool-literals, - modernize-use-nullptr, - modernize-use-using, - modernize-use-override, + modernize-use-constraints, + modernize-use-default-member-init, + modernize-use-designated-initializers, + modernize-use-emplace, modernize-use-equals-default, modernize-use-equals-delete, + modernize-use-integer-sign-comparison, + modernize-use-nodiscard, + modernize-use-noexcept, + modernize-use-nullptr, + modernize-use-override, + modernize-use-ranges, + modernize-use-scoped-lock, + modernize-use-starts-ends-with, + modernize-use-std-format, + modernize-use-std-numbers, + modernize-use-std-print, + modernize-use-transparent-functors, + modernize-use-uncaught-exceptions, + modernize-use-using, + misc-throw-by-value-catch-by-reference, misc-misplaced-const, @@ -92,6 +116,7 @@ Checks: readability-delete-null-pointer, readability-deleted-default, readability-misplaced-array-index, + readability-make-member-function-const, readability-non-const-parameter, readability-redundant-control-flow, readability-redundant-function-ptr-dereference, @@ -115,7 +140,10 @@ Checks: google-runtime-operator, hicpp-exception-baseclass, hicpp-uppercase-literal-suffix, + cppcoreguidelines-virtual-class-destructor, + cppcoreguidelines-pro-type-member-init, + security-api-misuse, FormatStyle: 'file' diff --git a/src/core/Document.hpp b/src/core/Document.hpp index ac21eb3..ddc9d54 100644 --- a/src/core/Document.hpp +++ b/src/core/Document.hpp @@ -22,20 +22,20 @@ class Document { ".tiff", ".tif", ".webp"}; constexpr static const char* PROJECT_FILE_NAME = "croplines.json"; - bool Load(const std::filesystem::path& path); - bool Save(); - bool Close(); - bool IsLoad() const { return m_data.has_value(); }; + bool Load(const std::filesystem::path& path); + bool Save(); + bool Close(); + [[nodiscard]] bool IsLoad() const { return m_data.has_value(); }; - const std::filesystem::path& GetPath() const { return cwd; } + [[nodiscard]] const std::filesystem::path& GetPath() const { return cwd; } - wxCommandProcessor* GetProcessor() const { return m_processor; } + [[nodiscard]] wxCommandProcessor* GetProcessor() const { return m_processor; } - bool IsModified() const { return m_modified; } - void SetModified(bool modified = true); + [[nodiscard]] bool IsModified() const { return m_modified; } + void SetModified(bool modified = true); - size_t PagesSize() const { return m_data->pages.size(); }; - Page LoadPage(size_t index); + [[nodiscard]] size_t PagesSize() const { return m_data->pages.size(); }; + Page LoadPage(size_t index); bool SaveAllCrops(); diff --git a/src/core/Event.hpp b/src/core/Event.hpp index edf62f4..79cbf3a 100644 --- a/src/core/Event.hpp +++ b/src/core/Event.hpp @@ -1,3 +1,5 @@ +#include + #include #include @@ -7,7 +9,7 @@ wxDECLARE_EVENT(EVT_DOCUMENT_CHANGED, wxCommandEvent); class DocumentEvent final : public wxCommandEvent { public: - enum Type { Loaded, Saved, UndoDone, RedoDone, Modified }; + enum Type : std::uint8_t { Loaded, Saved, UndoDone, RedoDone, Modified }; DocumentEvent(Type actionType) : wxCommandEvent(EVT_DOCUMENT_CHANGED), m_type(actionType) {} DocumentEvent(const DocumentEvent& other) = default; diff --git a/src/core/ImageScaleModel.cpp b/src/core/ImageScaleModel.cpp index 6e133f3..330e29d 100644 --- a/src/core/ImageScaleModel.cpp +++ b/src/core/ImageScaleModel.cpp @@ -92,3 +92,6 @@ bool ImageScaleModel::IsInsideImage(wxRealPoint worldPoint) const { return 0 <= imagePoint.x && imagePoint.x <= imageSize.GetWidth() && 0 <= imagePoint.y && imagePoint.y <= imageSize.GetHeight(); } +wxRealPoint croplines::ImageScaleModel::Transform(wxRealPoint point) const { + return scale * point + wxRealPoint(offset); +} diff --git a/src/core/ImageScaleModel.hpp b/src/core/ImageScaleModel.hpp index cfd88be..55f1c48 100644 --- a/src/core/ImageScaleModel.hpp +++ b/src/core/ImageScaleModel.hpp @@ -20,7 +20,6 @@ class ImageScaleModel { double scale; wxPoint offset; - ImageScaleModel() = default; ImageScaleModel(wxSize imageSize, wxSize windowSize); ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale); @@ -33,22 +32,22 @@ class ImageScaleModel { void OnWindowResize(wxSize windowSizeNew); void OnImageResize(wxSize imageSizeNew); - double GetScaleSuitesPage() const; - double GetScaleSuitesWidth() const; - double GetScaleSuitesHeight() const; + [[nodiscard]] double GetScaleSuitesPage() const; + [[nodiscard]] double GetScaleSuitesWidth() const; + [[nodiscard]] double GetScaleSuitesHeight() const; - cv::Mat GetTransformMatrix() const; + [[nodiscard]] cv::Mat GetTransformMatrix() const; - wxRealPoint Transform(wxRealPoint point) const { return scale * point + wxRealPoint(offset); } - double TransformX(double x) const { return scale * x + offset.x; } - double TransformY(double y) const { return scale * y + offset.y; } - wxRealPoint ReverseTransform(wxRealPoint point) const { + [[nodiscard]] wxRealPoint Transform(wxRealPoint point) const; + [[nodiscard]] double TransformX(double x) const { return scale * x + offset.x; } + [[nodiscard]] double TransformY(double y) const { return scale * y + offset.y; } + [[nodiscard]] wxRealPoint ReverseTransform(wxRealPoint point) const { return (point - wxRealPoint(offset)) / scale; } - double ReverseTransformX(double x) const { return (x - offset.x) / scale; } - double ReverseTransformY(double y) const { return (y - offset.y) / scale; } + [[nodiscard]] double ReverseTransformX(double x) const { return (x - offset.x) / scale; } + [[nodiscard]] double ReverseTransformY(double y) const { return (y - offset.y) / scale; } - bool IsInsideImage(wxRealPoint worldPoint) const; + [[nodiscard]] bool IsInsideImage(wxRealPoint worldPoint) const; private: void Clamp(); diff --git a/src/core/Page.cpp b/src/core/Page.cpp index e50b28e..561dbda 100644 --- a/src/core/Page.cpp +++ b/src/core/Page.cpp @@ -93,7 +93,7 @@ std::optional Page::SearchNearestLine(int searchPosition, int threshold) co filter_noise_size: 忽略黑像素的大小 expand_size: 留边空白大小 */ -static std::optional CalculateSelectArea(cv::Mat image, int filter_noise_size, +static std::optional CalculateSelectArea(const cv::Mat& image, int filter_noise_size, int expand_size, int base_line) { cv::Mat img_dst; cv::cvtColor(image, img_dst, cv::COLOR_RGB2GRAY); diff --git a/src/ui/Canvas.cpp b/src/ui/Canvas.cpp index 6b9d6a4..294c56e 100644 --- a/src/ui/Canvas.cpp +++ b/src/ui/Canvas.cpp @@ -37,7 +37,7 @@ void Canvas::SetPage(Page& page) { return; } if (IsLoaded()) { - m_scaleModel.OnImageResize(img.GetSize()); + GetScaleModel().OnImageResize(img.GetSize()); } else { m_scaleModel = ImageScaleModel{img.GetSize(), GetClientSize()}; } @@ -53,31 +53,32 @@ void Canvas::SetPage(Page& page) { void Canvas::Clear() { if (m_glTexture) glDeleteTextures(1, &m_glTexture); m_page = nullptr; + m_scaleModel.reset(); Refresh(); } void Canvas::ZoomIn() { if (!IsLoaded()) return; - m_scaleModel.Scale(ImageScaleModel::ZOOM_IN_RATE); + GetScaleModel().Scale(ImageScaleModel::ZOOM_IN_RATE); Refresh(); } void Canvas::ZoomOut() { if (!IsLoaded()) return; - m_scaleModel.Scale(ImageScaleModel::ZOOM_OUT_RATE); + GetScaleModel().Scale(ImageScaleModel::ZOOM_OUT_RATE); Refresh(); } void Canvas::ZoomFit() { if (!IsLoaded()) return; - m_scaleModel.ScaleTo(m_scaleModel.GetScaleSuitesPage()); - m_scaleModel.MoveToCenter(); + GetScaleModel().ScaleTo(GetScaleModel().GetScaleSuitesPage()); + GetScaleModel().MoveToCenter(); Refresh(); } void Canvas::Zoom(double scale) { if (IsLoaded()) { - m_scaleModel.ScaleTo(scale); + GetScaleModel().ScaleTo(scale); Refresh(); } } @@ -88,15 +89,15 @@ void Canvas::UpdateScrollbars() { SetScrollbar(wxVERTICAL, -1, -1, -1); return; } - if (m_scaleModel.scaledSize.GetWidth() > m_scaleModel.windowSize.GetWidth()) { - SetScrollbar(wxHORIZONTAL, -m_scaleModel.offset.x, m_scaleModel.windowSize.GetWidth(), - m_scaleModel.scaledSize.GetWidth()); + if (GetScaleModel().scaledSize.GetWidth() > GetScaleModel().windowSize.GetWidth()) { + SetScrollbar(wxHORIZONTAL, -GetScaleModel().offset.x, GetScaleModel().windowSize.GetWidth(), + GetScaleModel().scaledSize.GetWidth()); } else { SetScrollbar(wxHORIZONTAL, -1, -1, -1); } - if (m_scaleModel.scaledSize.GetHeight() > m_scaleModel.windowSize.GetHeight()) { - SetScrollbar(wxVERTICAL, -m_scaleModel.offset.y, m_scaleModel.windowSize.GetHeight(), - m_scaleModel.scaledSize.GetHeight()); + if (GetScaleModel().scaledSize.GetHeight() > GetScaleModel().windowSize.GetHeight()) { + SetScrollbar(wxVERTICAL, -GetScaleModel().offset.y, GetScaleModel().windowSize.GetHeight(), + GetScaleModel().scaledSize.GetHeight()); } else { SetScrollbar(wxVERTICAL, -1, -1, -1); } @@ -154,12 +155,12 @@ void Canvas::OnPaint(wxPaintEvent&) { // 设置投影(透视投影) glMatrixMode(GL_MODELVIEW); glLoadIdentity(); - glTranslated(m_scaleModel.offset.x, m_scaleModel.offset.y, 0); - glScaled(m_scaleModel.scale, m_scaleModel.scale, 1.0); + glTranslated(GetScaleModel().offset.x, GetScaleModel().offset.y, 0); + glScaled(GetScaleModel().scale, GetScaleModel().scale, 1.0); if (IsLoaded()) { // 绑定纹理 - wxSize size = m_scaleModel.imageSize; + wxSize size = GetScaleModel().imageSize; glBegin(GL_QUADS); glColor3d(1.0, 1.0, 1.0); glBindTexture(GL_TEXTURE_2D, m_glTexture); @@ -186,7 +187,7 @@ void Canvas::OnPaint(wxPaintEvent&) { } auto DrawLine = [&size, this](int width, double line_y) { - double w = FromDIP(width) / m_scaleModel.scale; + double w = FromDIP(width) / GetScaleModel().scale; glBegin(GL_QUADS); glVertex2d(0, line_y - w); glVertex2d(0, line_y + w); @@ -199,10 +200,10 @@ void Canvas::OnPaint(wxPaintEvent&) { std::optional deleting_line; if (m_isDeleting && m_mouseCurrentPosition) { int y = m_mouseCurrentPosition->y; - y = std::lround(m_scaleModel.ReverseTransformY(y)); + y = std::lround(GetScaleModel().ReverseTransformY(y)); auto search_line = m_page->SearchNearestLine( - y, FromDIP(static_cast(5.0 / m_scaleModel.scale) + 1)); + y, FromDIP(static_cast(5.0 / GetScaleModel().scale) + 1)); if (search_line.has_value()) { deleting_line = *search_line; glColor4d(0.15, 0.58, 0.36, 0.5); @@ -222,7 +223,7 @@ void Canvas::OnPaint(wxPaintEvent&) { glColor4d(0.90, 0.08, 0, 0.5); else glColor4d(0.34, 0.61, 0.84, 0.5); - double y = m_scaleModel.ReverseTransformY(m_mouseCurrentPosition->y); + double y = GetScaleModel().ReverseTransformY(m_mouseCurrentPosition->y); DrawLine(2, y); } } @@ -235,7 +236,7 @@ void Canvas::OnSize(wxSizeEvent& event) { SetCurrent(*m_glContext); UpdateProjection(GetClientSize()); } - if (IsLoaded()) m_scaleModel.OnWindowResize(event.GetSize()); + if (IsLoaded()) GetScaleModel().OnWindowResize(event.GetSize()); Refresh(); } @@ -248,7 +249,7 @@ void Canvas::OnMouseWheel(wxMouseEvent& event) { } else { // zoom out factor = ImageScaleModel::ZOOM_OUT_RATE; } - m_scaleModel.Scale(factor, event.GetPosition()); + GetScaleModel().Scale(factor, event.GetPosition()); Refresh(); } @@ -313,12 +314,12 @@ void Canvas::OnMouseRightUp(wxMouseEvent& event) { if (!IsLoaded()) return; wxPoint mousePosition = event.GetPosition(); - if (!m_scaleModel.IsInsideImage(mousePosition)) return; + if (!GetScaleModel().IsInsideImage(mousePosition)) return; int y = mousePosition.y; - y = std::lround(m_scaleModel.ReverseTransformY(y)); + y = std::lround(GetScaleModel().ReverseTransformY(y)); if (m_isDeleting) { - int threshold = static_cast(5.0 / m_scaleModel.scale) + 1; + int threshold = static_cast(5.0 / GetScaleModel().scale) + 1; auto line = m_page->SearchNearestLine(y, FromDIP(threshold)); if (line.has_value()) GetProcessor()->Submit(new EraseLineCommand{GetPage(), *line}); } else { @@ -333,14 +334,14 @@ void Canvas::OnMouseMotion(wxMouseEvent& event) { wxPoint mouse_drag = event.GetPosition(); if (m_mouseDragStartPosition && event.Dragging()) { auto dr = mouse_drag - *m_mouseDragStartPosition; - m_scaleModel.Move(dr); + GetScaleModel().Move(dr); *m_mouseDragStartPosition = mouse_drag; Refresh(); } // if mouse inside image wxPoint mouse_position = event.GetPosition(); - if (m_scaleModel.IsInsideImage(mouse_position)) { + if (GetScaleModel().IsInsideImage(mouse_position)) { m_mouseCurrentPosition = mouse_position; Refresh(); } else if (m_mouseCurrentPosition) { @@ -357,14 +358,14 @@ void Canvas::OnScroll(wxScrollWinEvent& event) { case wxHORIZONTAL: { const int pos0 = GetScrollPos(wxHORIZONTAL); const int pos1 = event.GetPosition(); - m_scaleModel.Move(wxPoint{pos0 - pos1, 0}); + GetScaleModel().Move(wxPoint{pos0 - pos1, 0}); Refresh(); return; } case wxVERTICAL: { const int pos0 = GetScrollPos(wxVERTICAL); const int pos1 = event.GetPosition(); - m_scaleModel.Move(wxPoint{0, pos0 - pos1}); + GetScaleModel().Move(wxPoint{0, pos0 - pos1}); Refresh(); return; } diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp index 28a2f7f..7f1d9a2 100644 --- a/src/ui/Canvas.hpp +++ b/src/ui/Canvas.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -20,9 +22,9 @@ class Canvas : public wxGLCanvas { Document& GetDocument() { return GetPage().GetDocument(); } wxCommandProcessor* GetProcessor() { return GetDocument().GetProcessor(); } - void Clear(); - bool IsLoaded() const { return m_page != nullptr; } - ImageScaleModel& getScaleModel() { return m_scaleModel; } + void Clear(); + [[nodiscard]] bool IsLoaded() const { return m_page != nullptr; } + ImageScaleModel& GetScaleModel() { return m_scaleModel.value(); } void ZoomIn(); void ZoomOut(); @@ -37,11 +39,12 @@ class Canvas : public wxGLCanvas { std::optional m_mouseDragStartPosition; std::optional m_mouseCurrentPosition; - Page* m_page = nullptr; - ImageScaleModel m_scaleModel; + Page* m_page = nullptr; + + std::optional m_scaleModel; wxGLContext* m_glContext; - GLuint m_glTexture; + GLuint m_glTexture = 0; wxImage m_imageDst; bool m_isImageModified = false; diff --git a/src/ui/Defs.hpp b/src/ui/Defs.hpp index 3993d93..fedc4b5 100644 --- a/src/ui/Defs.hpp +++ b/src/ui/Defs.hpp @@ -1,7 +1,7 @@ #pragma once namespace croplines { -enum { +enum : int { buttonID_CROP_CURR_PAGE = 1000, buttonID_CROP_ALL_PAGE, diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp index d504c13..e2a0e36 100644 --- a/src/ui/MainFrame.cpp +++ b/src/ui/MainFrame.cpp @@ -82,7 +82,7 @@ MainFrame::MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, con .MinSize(FromDIP(wxSize(350, -1)))); m_pageListPanel = new wxListBox{this, panelID_PAGE_LIST, wxDefaultPosition, wxDefaultSize, - 0, nullptr, wxLB_NEEDED_SB}; + 0, nullptr, wxLB_NEEDED_SB}; m_mgr.AddPane(m_pageListPanel, wxAuiPaneInfo() .Left() .Caption(wxT("页面列表")) @@ -103,7 +103,7 @@ MainFrame::MainFrame(wxWindow* parent, wxWindowID id, const wxString& title, con m_menuBar->Disable(); } -bool MainFrame::Load(std::filesystem::path path) { +bool MainFrame::Load(const std::filesystem::path& path) { if (m_doc.IsLoad()) { if (!Close()) return false; } diff --git a/src/ui/MainFrame.hpp b/src/ui/MainFrame.hpp index eabe239..6146956 100644 --- a/src/ui/MainFrame.hpp +++ b/src/ui/MainFrame.hpp @@ -36,14 +36,14 @@ class MainFrame final : public wxFrame { Document m_doc; std::optional m_currentPage; - bool Load(std::filesystem::path path); + bool Load(const std::filesystem::path& path); bool Save(); bool Close(); - std::size_t CurrentPage() { return m_currentPageIdx; } - void CurrentPage(std::size_t); - void PrevPage() { CurrentPage(CurrentPage() - 1); } - void NextPage() { CurrentPage(CurrentPage() + 1); } + [[nodiscard]] std::size_t CurrentPage() const { return m_currentPageIdx; } + void CurrentPage(std::size_t); + void PrevPage() { CurrentPage(CurrentPage() - 1); } + void NextPage() { CurrentPage(CurrentPage() + 1); } void SyncUIPageListPanel(); void SyncUIConfigPanel(); diff --git a/src/ui/components/SliderWithSpin.hpp b/src/ui/components/SliderWithSpin.hpp index e64cbcf..65a5c4a 100644 --- a/src/ui/components/SliderWithSpin.hpp +++ b/src/ui/components/SliderWithSpin.hpp @@ -16,14 +16,14 @@ class SliderWithSpin : public wxPanel { bool Enable(bool enable = false) override; - int GetValue() const { return m_value; } - void SetValue(int value); + [[nodiscard]] int GetValue() const { return m_value; } + void SetValue(int value); private: - wxStaticText* m_label; - wxSlider* m_slider; - wxSpinCtrl* m_spin; - int m_value; + wxStaticText* m_label = nullptr; + wxSlider* m_slider = nullptr; + wxSpinCtrl* m_spin = nullptr; + int m_value = 0; void CallEvent(int value); void OnSliderChanging(wxCommandEvent&); From d4af707631da07f6eabe74b31473ae5d94fe3638 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 02:23:18 +0800 Subject: [PATCH 06/16] Update: Impose strict clang tidy rules (continue last work) --- .clang-tidy | 24 ++++++++++++++++-------- src/core/Event.hpp | 5 ++--- src/core/ImageScaleModel.cpp | 7 ------- src/core/ImageScaleModel.hpp | 3 ++- src/ui/Canvas.cpp | 12 ++++++------ src/ui/Canvas.hpp | 10 ++++++---- src/ui/ConfigPanel.cpp | 8 ++++---- 7 files changed, 36 insertions(+), 33 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index c6f147e..2fa0ccd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,4 @@ -Checks: - -*, +Checks: -*, bugprone-undelegated-constructor, bugprone-argument-comment, bugprone-bad-signal-to-kill-thread, @@ -97,18 +96,20 @@ Checks: modernize-use-uncaught-exceptions, modernize-use-using, - - misc-throw-by-value-catch-by-reference, + misc-const-correctness, + misc-definitions-in-headers, + misc-header-include-cycle, + misc-include-cleaner, misc-misplaced-const, - misc-unconventional-assign-operator, misc-redundant-expression, misc-static-assert, + misc-throw-by-value-catch-by-reference, misc-unconventional-assign-operator, misc-uniqueptr-reset-release, misc-unused-alias-decls, misc-unused-parameters, misc-unused-using-decls, - misc-definitions-in-headers + misc-use-internal-linkage, readability-avoid-const-params-in-decls readability-const-return-type, @@ -136,14 +137,21 @@ Checks: cert-mem57-cpp, cert-oop58-cpp, cert-err34-c, + google-build-explicit-make-pair, google-runtime-operator, + hicpp-exception-baseclass, hicpp-uppercase-literal-suffix, - cppcoreguidelines-virtual-class-destructor, + cppcoreguidelines-macro-usage, + cppcoreguidelines-misleading-capture-default-by-value, + cppcoreguidelines-missing-std-forward, + cppcoreguidelines-no-suspend-with-lock, cppcoreguidelines-pro-type-member-init, + cppcoreguidelines-rvalue-reference-param-not-moved, + cppcoreguidelines-virtual-class-destructor, security-api-misuse, -FormatStyle: 'file' +FormatStyle: "file" diff --git a/src/core/Event.hpp b/src/core/Event.hpp index 79cbf3a..37de473 100644 --- a/src/core/Event.hpp +++ b/src/core/Event.hpp @@ -12,11 +12,10 @@ class DocumentEvent final : public wxCommandEvent { enum Type : std::uint8_t { Loaded, Saved, UndoDone, RedoDone, Modified }; DocumentEvent(Type actionType) : wxCommandEvent(EVT_DOCUMENT_CHANGED), m_type(actionType) {} - DocumentEvent(const DocumentEvent& other) = default; - DocumentEvent* Clone() const override { return new DocumentEvent(*this); } + [[nodiscard]] DocumentEvent* Clone() const override { return new DocumentEvent(*this); } - Type GetActionType() const { return m_type; } + [[nodiscard]] Type GetActionType() const { return m_type; } private: Type m_type; diff --git a/src/core/ImageScaleModel.cpp b/src/core/ImageScaleModel.cpp index 330e29d..172d30e 100644 --- a/src/core/ImageScaleModel.cpp +++ b/src/core/ImageScaleModel.cpp @@ -8,13 +8,6 @@ ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize, double sca MoveToCenter(); } -ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize) - : imageSize(imageSize), windowSize(windowSize) { - scale = GetScaleSuitesPage(); - scaledSize = imageSize * scale; - MoveToCenter(); -} - void ImageScaleModel::Clamp() { wxSize border = windowSize - scaledSize; if (border.x < 0) { diff --git a/src/core/ImageScaleModel.hpp b/src/core/ImageScaleModel.hpp index 55f1c48..e9bafa0 100644 --- a/src/core/ImageScaleModel.hpp +++ b/src/core/ImageScaleModel.hpp @@ -20,8 +20,9 @@ class ImageScaleModel { double scale; wxPoint offset; - ImageScaleModel(wxSize imageSize, wxSize windowSize); ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale); + ImageScaleModel(wxSize imageSize, wxSize windowSize) + : ImageScaleModel(imageSize, windowSize, GetScaleSuitesPage()) {} void Scale(double factor, wxPoint center); void Scale(double factor) { Scale(factor, wxPoint{} + windowSize / 2); } diff --git a/src/ui/Canvas.cpp b/src/ui/Canvas.cpp index 294c56e..0a3bebd 100644 --- a/src/ui/Canvas.cpp +++ b/src/ui/Canvas.cpp @@ -1,24 +1,24 @@ #include "ui/Canvas.hpp" +#include + #include using namespace croplines; Canvas::Canvas(wxWindow* parent, wxWindowID id) : wxGLCanvas(parent, id, nullptr, wxDefaultPosition, wxDefaultSize, - wxFULL_REPAINT_ON_RESIZE | wxVSCROLL | wxHSCROLL) { + wxFULL_REPAINT_ON_RESIZE | wxVSCROLL | wxHSCROLL), + m_glContext(std::make_unique(this)) { AlwaysShowScrollbars(); // disable on default SetScrollbar(wxHORIZONTAL, -1, -1, -1); SetScrollbar(wxVERTICAL, -1, -1, -1); - - m_glContext = new wxGLContext(this); } Canvas::~Canvas() { if (m_glTexture) glDeleteTextures(1, &m_glTexture); - delete m_glContext; } static void SetTextrue(GLuint texture, void* pixels, int width, int height) { @@ -126,7 +126,7 @@ static void InitGL() { glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); } -void UpdateProjection(wxSize size) { +static void UpdateProjection(wxSize size) { if (size.GetWidth() <= 0 || size.GetHeight() <= 0) return; glViewport(0, 0, size.GetWidth(), size.GetHeight()); @@ -353,7 +353,7 @@ void Canvas::OnMouseMotion(wxMouseEvent& event) { void Canvas::OnScroll(wxScrollWinEvent& event) { if (!IsLoaded()) return; - const int orientation = event.GetOrientation() & wxORIENTATION_MASK; + const unsigned orientation = static_cast(event.GetOrientation()) & wxORIENTATION_MASK; switch (orientation & wxORIENTATION_MASK) { case wxHORIZONTAL: { const int pos0 = GetScrollPos(wxHORIZONTAL); diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp index 7f1d9a2..c7c38ab 100644 --- a/src/ui/Canvas.hpp +++ b/src/ui/Canvas.hpp @@ -1,10 +1,12 @@ #pragma once +#include #include #include #include #include +#include #include #include "core/Document.hpp" @@ -43,10 +45,10 @@ class Canvas : public wxGLCanvas { std::optional m_scaleModel; - wxGLContext* m_glContext; - GLuint m_glTexture = 0; - wxImage m_imageDst; - bool m_isImageModified = false; + std::unique_ptr m_glContext; + GLuint m_glTexture = 0; + wxImage m_imageDst; + bool m_isImageModified = false; private: void UpdateScrollbars(); diff --git a/src/ui/ConfigPanel.cpp b/src/ui/ConfigPanel.cpp index ef88021..5b99b0a 100644 --- a/src/ui/ConfigPanel.cpp +++ b/src/ui/ConfigPanel.cpp @@ -13,10 +13,10 @@ using namespace croplines; -ConfigPanel::ConfigPanel(wxWindow* parent, wxWindowID id) : wxNotebook(parent, id) { - m_processPage = new ProcessConfigPage{this, wxID_ANY}; - m_outputPage = new OutputConfigPage{this, wxID_ANY}; - +ConfigPanel::ConfigPanel(wxWindow* parent, wxWindowID id) + : wxNotebook(parent, id), + m_processPage(new ProcessConfigPage{this, wxID_ANY}), + m_outputPage(new OutputConfigPage{this, wxID_ANY}) { AddPage(m_processPage, wxT("处理")); AddPage(m_outputPage, wxT("输出")); } From ea152e05c4fe44ccfe20a01025226695c3651b93 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 03:36:37 +0800 Subject: [PATCH 07/16] Fix: Rename CMakeLists --- .github/workflows/cpp-linter.yml | 52 ++++++++++++++--- Cmakelists.txt => CMakeLists.txt | 96 +++++++++++++++++--------------- 2 files changed, 93 insertions(+), 55 deletions(-) rename Cmakelists.txt => CMakeLists.txt (89%) diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 6468a2f..372657a 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -2,10 +2,36 @@ name: cpp-linter on: pull_request: branches: [main] - paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake'] + paths: + [ + "**.c", + "**.cpp", + "**.h", + "**.hpp", + "**.cxx", + "**.hxx", + "**.cc", + "**.hh", + "**CMakeLists.txt", + "meson.build", + "**.cmake", + ] push: branches: [main] - paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake'] + paths: + [ + "**.c", + "**.cpp", + "**.h", + "**.hpp", + "**.cxx", + "**.hxx", + "**.cc", + "**.hh", + "**CMakeLists.txt", + "meson.build", + "**.cmake", + ] jobs: cpp-linter: @@ -22,23 +48,31 @@ jobs: libeigen3-dev \ libomp-dev \ build-essential - + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ runner.os }}-ccache-${{ hashFiles('**/CMakeLists.txt') }} + restore-keys: | + ${{ runner.os }}-ccache- + - name: Generate Compilation Database run: | cmake -S . -B build \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DCMAKE_BUILD_TYPE=Debug - + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + - uses: cpp-linter/cpp-linter-action@v2 id: linter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - style: 'file' # Use .clang-format config file - tidy-checks: '' # Use .clang-tidy config file + style: "file" # Use .clang-format config file + tidy-checks: "" # Use .clang-tidy config file # only 'update' a single comment in a pull request thread. thread-comments: ${{ github.event_name == 'pull_request' && 'update' }} - database: 'build' + database: "build" - name: Fail fast?! if: steps.linter.outputs.checks-failed > 0 diff --git a/Cmakelists.txt b/CMakeLists.txt similarity index 89% rename from Cmakelists.txt rename to CMakeLists.txt index 2b49914..8e37c30 100644 --- a/Cmakelists.txt +++ b/CMakeLists.txt @@ -1,46 +1,50 @@ -cmake_minimum_required(VERSION 3.21) -project(Croplines) - -add_compile_options("$<$:/source-charset:utf-8>") - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) - set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static-libgcc") - set(CMAKE_CXX_FLAGS_RELEASE "-s") -endif() - -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) - set(wxWidgets_USE_DEBUG ON) -else() - set(wxWidgets_USE_STATIC ON) -endif() -find_package(wxWidgets REQUIRED aui core base gl) -find_package(Eigen3 REQUIRED) # required by opencv -find_package(OpenCV REQUIRED) -find_package(OpenMP REQUIRED) -include(${wxWidgets_USE_FILE}) -include_directories(${OpenCV_INCLUDE_DIRS}) - -file(GLOB_RECURSE SRCS src/*.cpp) -if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") -set (WIN32_RESOURCES src/resource.rc) -endif() - -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) -add_executable(Croplines ${SRCS} ${WIN32_RESOURCES}) -else() -add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) -endif() -target_include_directories(Croplines PRIVATE src) -target_link_libraries(Croplines ${wxWidgets_LIBRARIES}) -target_link_libraries(Croplines ${OpenCV_LIBS}) -target_link_libraries(Croplines opengl32) - - -if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) - target_link_libraries(Croplines OpenMP::OpenMP_CXX -static-libgcc - -static-libstdc++ - -Wl,-Bstatic -lgomp -Wl,-Bdynamic) - target_link_libraries(Croplines -Wl,-Bstatic stdc++ pthread -Wl,-Bdynamic) # 解决libwinpthread-1.dll依赖问题 -endif() +cmake_minimum_required(VERSION 3.21) +project(Croplines) + +add_compile_options("$<$:/source-charset:utf-8>") + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) + set(CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static-libgcc") + set(CMAKE_CXX_FLAGS_RELEASE "-s") +endif() + +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) + set(wxWidgets_USE_DEBUG ON) +else() + set(wxWidgets_USE_STATIC ON) +endif() +find_package(wxWidgets REQUIRED aui core base gl) +find_package(Eigen3 REQUIRED) # required by opencv +find_package(OpenCV REQUIRED) +find_package(OpenMP REQUIRED) +find_package(cereal REQUIRED) +include(${wxWidgets_USE_FILE}) + +file(GLOB_RECURSE SRCS src/*.cpp) +if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") +set (WIN32_RESOURCES src/resource.rc) +endif() + +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) +add_executable(Croplines ${SRCS} ${WIN32_RESOURCES}) +else() +add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) +endif() +target_include_directories(Croplines PRIVATE src) +target_include_directories(Croplines PRIVATE cereal::cereal) +target_link_libraries(Croplines ${wxWidgets_LIBRARIES}) +# OpenCV +target_link_libraries(Croplines ${OpenCV_LIBS}) +target_include_directories(Croplines PRIVATE ${OpenCV_INCLUDE_DIRS}) +# OpenGL +target_link_libraries(Croplines opengl32) + + +if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) + target_link_libraries(Croplines OpenMP::OpenMP_CXX -static-libgcc + -static-libstdc++ + -Wl,-Bstatic -lgomp -Wl,-Bdynamic) + target_link_libraries(Croplines -Wl,-Bstatic stdc++ pthread -Wl,-Bdynamic) # 解决libwinpthread-1.dll依赖问题 +endif() From 048298737eb88d0473cd24fc78ac5ac5ef3b10ef Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 03:44:12 +0800 Subject: [PATCH 08/16] CI: Enhance CMakeLists and cpp-linter with additional dependencies --- .github/workflows/cpp-linter.yml | 3 +++ CMakeLists.txt | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 372657a..a9fd96f 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -43,10 +43,13 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ + libcereal-dev \ libwxgtk3.2-dev \ libopencv-dev \ libeigen3-dev \ libomp-dev \ + libgl1-mesa-dev \ + libglu1-mesa-dev \ build-essential - name: ccache diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e37c30..9e5935c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,9 @@ endif() if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) set(wxWidgets_USE_DEBUG ON) else() - set(wxWidgets_USE_STATIC ON) + if(MSVC OR WIN32) + set(wxWidgets_USE_STATIC ON) + endif() endif() find_package(wxWidgets REQUIRED aui core base gl) find_package(Eigen3 REQUIRED) # required by opencv From 271ed213e8e96aa405de906205f5896450bae0fa Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 03:56:34 +0800 Subject: [PATCH 09/16] Fix: Organize header --- src/core/Document.cpp | 8 ++++++-- src/core/Document.hpp | 2 -- src/ui/Canvas.hpp | 1 - src/ui/MainFrame.cpp | 2 +- src/ui/MenuBar.hpp | 1 - 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/Document.cpp b/src/core/Document.cpp index 9d908c0..0411770 100644 --- a/src/core/Document.cpp +++ b/src/core/Document.cpp @@ -1,15 +1,19 @@ #include "core/Document.hpp" #include +#include #include #include #include #include #include +#include #include +#include #include "core/DocumentData.hpp" +#include "core/Page.hpp" #include "utils/Asserts.hpp" #include "utils/Compare.hpp" @@ -24,7 +28,7 @@ bool Document::Load(const fs::path& path) { m_data.emplace(); - fs::path prj_path = path / PROJECT_FILE_NAME; + const fs::path prj_path = path / PROJECT_FILE_NAME; if (fs::exists(prj_path) && fs::is_regular_file(prj_path)) { std::ifstream file(prj_path); if (file) { @@ -62,7 +66,7 @@ void Document::InitializeEmptyProject() { bool Document::Save() { ASSERT_WITH(IsLoad(), "Project not loaded!"); - fs::path prj_path = cwd / PROJECT_FILE_NAME; + const fs::path prj_path = cwd / PROJECT_FILE_NAME; std::ofstream file(prj_path); cereal::JSONOutputArchive archive(file); diff --git a/src/core/Document.hpp b/src/core/Document.hpp index ddc9d54..b1b1110 100644 --- a/src/core/Document.hpp +++ b/src/core/Document.hpp @@ -5,9 +5,7 @@ #include #include -#include #include -#include #include #include "core/DocumentData.hpp" diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp index c7c38ab..fc44af8 100644 --- a/src/ui/Canvas.hpp +++ b/src/ui/Canvas.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include "core/Document.hpp" diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp index e2a0e36..cc057d7 100644 --- a/src/ui/MainFrame.cpp +++ b/src/ui/MainFrame.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include diff --git a/src/ui/MenuBar.hpp b/src/ui/MenuBar.hpp index a76fa6b..081705b 100644 --- a/src/ui/MenuBar.hpp +++ b/src/ui/MenuBar.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include namespace croplines { class MenuBar : public wxMenuBar { From f4597375d37721c3a9bb7eae31ae2f334f292312 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 13:55:03 +0800 Subject: [PATCH 10/16] Fix: clang-tidy warning --- .zed/settings.json | 26 ++++++++------------------ src/core/Document.cpp | 2 +- src/core/Document.hpp | 3 --- src/core/DocumentData.hpp | 4 +++- src/core/Event.hpp | 24 ------------------------ src/core/ImageScaleModel.hpp | 1 + src/ui/MainFrame.hpp | 2 +- src/utils/Compare.cpp | 2 +- 8 files changed, 15 insertions(+), 49 deletions(-) delete mode 100644 src/core/Event.hpp diff --git a/.zed/settings.json b/.zed/settings.json index 3e6ff72..1fd2cea 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -7,24 +7,14 @@ "clangd": { "binary": { "arguments": [ - "--query-driver=D:\\MSYS2\\ucrt64\\bin\\g*", - "--all-scopes-completion", // 全局补全(补全建议会给出在当前作用域不可见的索引,插入后自动补充作用域标识符),例如在main()中直接写cout,即使没有`#include `,也会给出`std::cout`的建议,配合"--header-insertion=iwyu",还可自动插入缺失的头文件 - "--background-index", // 后台分析并保存索引文件 - "--clang-tidy", // 启用 Clang-Tidy 以提供「静态检查」,下面设置 clang tidy 规则 - "--clang-tidy-checks=performance-*, bugprone-*, misc-*, google-*, modernize-*, readability-*, portability-*", - "--compile-commands-dir=./build/", // 编译数据库(例如 compile_commands.json 文件)的目录位置 - "--completion-parse=auto", // 当 clangd 准备就绪时,用它来分析建议 - "--completion-style=detailed", // 建议风格:打包(重载函数只会给出一个建议);还可以设置为 detailed - "--enable-config", // 启用配置文件(YAML格式)项目配置文件是在项目文件夹里的“.clangd”,用户配置文件是“clangd/config.yaml”,该文件来自:Windows: %USERPROFILE%\AppData\Local || MacOS: ~/Library/Preferences/ || Others: $XDG_CONFIG_HOME, usually ~/.config - "--function-arg-placeholders=true", // 补全函数时,将会给参数提供占位符,键入后按 Tab 可以切换到下一占位符,乃至函数末 - "--header-insertion-decorators", // 输入建议中,已包含头文件的项与还未包含头文件的项会以圆点加以区分 - "--header-insertion=iwyu", // 插入建议时自动引入头文件 iwyu - "--include-cleaner-stdlib", // 为标准库头文件启用清理功能(不成熟!!!) - "--log=verbose", // 让 Clangd 生成更详细的日志 - "--pch-storage=memory", // pch 优化的位置(Memory 或 Disk,前者会增加内存开销,但会提升性能) - "--pretty", // 输出的 JSON 文件更美观 - "--ranking-model=decision_forest", // 建议的排序方案:hueristics (启发式), decision_forest (决策树) - "-j=12" // 同时开启的任务数量 + "--query-driver=D:/MSYS2/ucrt64/bin/g++.exe", + "--background-index", + "--all-scopes-completion", + "--completion-style=detailed", + "--clang-tidy", + "--pch-storage=memory", + "--compile-commands-dir=./build/", + "-j=12" ] } } diff --git a/src/core/Document.cpp b/src/core/Document.cpp index 0411770..0894432 100644 --- a/src/core/Document.cpp +++ b/src/core/Document.cpp @@ -50,7 +50,7 @@ void Document::InitializeEmptyProject() { }; for (auto const& entry : fs::directory_iterator(cwd) | std::views::filter(extensionFilter)) { fs::path relativePath = fs::relative(entry.path(), cwd); - m_data->pages.push_back(std::make_unique(relativePath)); + m_data->pages.push_back(std::make_unique(std::move(relativePath))); } // sort pages diff --git a/src/core/Document.hpp b/src/core/Document.hpp index b1b1110..27f1d49 100644 --- a/src/core/Document.hpp +++ b/src/core/Document.hpp @@ -5,11 +5,8 @@ #include #include -#include -#include #include "core/DocumentData.hpp" -#include "core/Event.hpp" #include "core/Page.hpp" namespace croplines { diff --git a/src/core/DocumentData.hpp b/src/core/DocumentData.hpp index f362bc0..b42ce58 100644 --- a/src/core/DocumentData.hpp +++ b/src/core/DocumentData.hpp @@ -1,12 +1,14 @@ #pragma once +#include #include #include #include +#include +#include #include #include -#include #include #include diff --git a/src/core/Event.hpp b/src/core/Event.hpp deleted file mode 100644 index 37de473..0000000 --- a/src/core/Event.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#include - -#include -#include - -namespace croplines { - -wxDECLARE_EVENT(EVT_DOCUMENT_CHANGED, wxCommandEvent); - -class DocumentEvent final : public wxCommandEvent { - public: - enum Type : std::uint8_t { Loaded, Saved, UndoDone, RedoDone, Modified }; - - DocumentEvent(Type actionType) : wxCommandEvent(EVT_DOCUMENT_CHANGED), m_type(actionType) {} - - [[nodiscard]] DocumentEvent* Clone() const override { return new DocumentEvent(*this); } - - [[nodiscard]] Type GetActionType() const { return m_type; } - - private: - Type m_type; -}; - -} // namespace croplines diff --git a/src/core/ImageScaleModel.hpp b/src/core/ImageScaleModel.hpp index e9bafa0..18e4e69 100644 --- a/src/core/ImageScaleModel.hpp +++ b/src/core/ImageScaleModel.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include namespace croplines { diff --git a/src/ui/MainFrame.hpp b/src/ui/MainFrame.hpp index 6146956..b65cab1 100644 --- a/src/ui/MainFrame.hpp +++ b/src/ui/MainFrame.hpp @@ -4,9 +4,9 @@ #include #include #include -#include #include #include +#include #include #include "core/Document.hpp" diff --git a/src/utils/Compare.cpp b/src/utils/Compare.cpp index 9f1b807..2846a69 100644 --- a/src/utils/Compare.cpp +++ b/src/utils/Compare.cpp @@ -2,7 +2,7 @@ #include -std::strong_ordering Croplines::NaturalCompare(std::string_view a, std::string_view b) { +std::strong_ordering croplines::NaturalCompare(std::string_view a, std::string_view b) { for (const char *i1 = a.begin(), *i2 = b.begin(); i1 != a.end() && i2 != b.end(); ++i1, ++i2) { if (std::isdigit(*i1) && std::isdigit(*i2)) { const char *ii1 = i1, *ii2 = i2; From ce8004aeb922483786fd38ac8c11f958bde88c97 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 15:28:59 +0800 Subject: [PATCH 11/16] CI: Use MSYS2 enviroment --- .github/workflows/cpp-linter.yml | 27 +++++++++++++++++---------- src/core/Document.cpp | 3 +++ src/core/DocumentData.hpp | 1 + src/core/ImageScaleModel.cpp | 4 ++++ src/ui/App.hpp | 1 - src/ui/MenuBar.cpp | 11 +++++++---- src/utils/Compare.cpp | 1 + 7 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index a9fd96f..872555b 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -35,10 +35,26 @@ on: jobs: cpp-linter: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - uses: actions/checkout@v5 + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: CLANG64 + update: true + install: >- + mingw-w64-clang64-x86_64-cmake + mingw-w64-clang64-x86_64-toolchain + mingw-w64-clang64-x86_64-cereal + mingw-w64-clang64-x86_64-wxwidgets3.3-common + mingw-w64-clang64-x86_64-wxwidgets3.3-common-libs + mingw-w64-clang64-x86_64-wxwidgets3.3-msw + mingw-w64-clang64-x86_64-wxwidgets3.3-msw-libs + mingw-w64-clang64-x86_64-opencv + mingw-w64-clang64-x86_64-eigen3 + - name: Install Dependencies run: | sudo apt-get update @@ -52,18 +68,9 @@ jobs: libglu1-mesa-dev \ build-essential - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - key: ${{ runner.os }}-ccache-${{ hashFiles('**/CMakeLists.txt') }} - restore-keys: | - ${{ runner.os }}-ccache- - - name: Generate Compilation Database run: | cmake -S . -B build \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - uses: cpp-linter/cpp-linter-action@v2 diff --git a/src/core/Document.cpp b/src/core/Document.cpp index 0894432..7c91065 100644 --- a/src/core/Document.cpp +++ b/src/core/Document.cpp @@ -4,10 +4,13 @@ #include #include #include +#include #include #include #include +#include #include +#include #include #include diff --git a/src/core/DocumentData.hpp b/src/core/DocumentData.hpp index b42ce58..c32d69a 100644 --- a/src/core/DocumentData.hpp +++ b/src/core/DocumentData.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/src/core/ImageScaleModel.cpp b/src/core/ImageScaleModel.cpp index 172d30e..d6ef3d2 100644 --- a/src/core/ImageScaleModel.cpp +++ b/src/core/ImageScaleModel.cpp @@ -1,5 +1,9 @@ #include "core/ImageScaleModel.hpp" +#include + +#include + using namespace croplines; ImageScaleModel::ImageScaleModel(wxSize imageSize, wxSize windowSize, double scale) diff --git a/src/ui/App.hpp b/src/ui/App.hpp index a482e3f..c491aeb 100644 --- a/src/ui/App.hpp +++ b/src/ui/App.hpp @@ -9,7 +9,6 @@ class CroplinesApp : public wxApp { MainFrame* m_frame; public: - // 这个函数将会在程序启动的时候被调用 bool OnInit() override; }; } // namespace croplines diff --git a/src/ui/MenuBar.cpp b/src/ui/MenuBar.cpp index 2a8e983..8ee22c2 100644 --- a/src/ui/MenuBar.cpp +++ b/src/ui/MenuBar.cpp @@ -1,5 +1,8 @@ #include "ui/MenuBar.hpp" +#include +#include + #include "ui/Defs.hpp" using namespace croplines; @@ -9,11 +12,11 @@ MenuBar::MenuBar() : wxMenuBar() { m_menuFile->Append(wxID_SAVE); m_menuFile->AppendSeparator(); m_menuFile->Append(buttonID_CROP_CURR_PAGE, wxT("Crop ¤t page"), - wxT("Crop current page to subimages and save each one to " - "output directory")); + wxT("Crop current page to subimages and save each one to " + "output directory")); m_menuFile->Append(buttonID_CROP_ALL_PAGE, wxT("Crop &all pages"), - wxT("Crop all pages to subimages and save each one to " - "output directory")); + wxT("Crop all pages to subimages and save each one to " + "output directory")); m_menuFile->AppendSeparator(); m_menuFile->Append(wxID_CLOSE); m_menuFile->Append(wxID_EXIT); diff --git a/src/utils/Compare.cpp b/src/utils/Compare.cpp index 2846a69..6d988ec 100644 --- a/src/utils/Compare.cpp +++ b/src/utils/Compare.cpp @@ -1,6 +1,7 @@ #include "utils/Compare.hpp" #include +#include std::strong_ordering croplines::NaturalCompare(std::string_view a, std::string_view b) { for (const char *i1 = a.begin(), *i2 = b.begin(); i1 != a.end() && i2 != b.end(); ++i1, ++i2) { From c7c86130a81f5c4a4751f09c07d46433d7e8728e Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 15:33:23 +0800 Subject: [PATCH 12/16] CI: Fix dependencies --- .github/workflows/cpp-linter.yml | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 872555b..c3133c9 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -45,28 +45,15 @@ jobs: msystem: CLANG64 update: true install: >- - mingw-w64-clang64-x86_64-cmake - mingw-w64-clang64-x86_64-toolchain - mingw-w64-clang64-x86_64-cereal - mingw-w64-clang64-x86_64-wxwidgets3.3-common - mingw-w64-clang64-x86_64-wxwidgets3.3-common-libs - mingw-w64-clang64-x86_64-wxwidgets3.3-msw - mingw-w64-clang64-x86_64-wxwidgets3.3-msw-libs - mingw-w64-clang64-x86_64-opencv - mingw-w64-clang64-x86_64-eigen3 - - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libcereal-dev \ - libwxgtk3.2-dev \ - libopencv-dev \ - libeigen3-dev \ - libomp-dev \ - libgl1-mesa-dev \ - libglu1-mesa-dev \ - build-essential + mingw-w64-clang-x86_64-cmake + mingw-w64-clang-x86_64-toolchain + mingw-w64-clang-x86_64-cereal + mingw-w64-clang-x86_64-wxwidgets3.3-common + mingw-w64-clang-x86_64-wxwidgets3.3-common-libs + mingw-w64-clang-x86_64-wxwidgets3.3-msw + mingw-w64-clang-x86_64-wxwidgets3.3-msw-libs + mingw-w64-clang-x86_64-opencv + mingw-w64-clang-x86_64-eigen3 - name: Generate Compilation Database run: | From 47811dcd9926a69dd2469cee24148b140b4e64e7 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 15:41:00 +0800 Subject: [PATCH 13/16] CI: FIx use MSYS2 Shell --- .github/workflows/cpp-linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index c3133c9..ba87232 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -56,6 +56,7 @@ jobs: mingw-w64-clang-x86_64-eigen3 - name: Generate Compilation Database + shell: msys2 {0} run: | cmake -S . -B build \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON From 612cd6705e2ab2824832ff50b9f0f80f0d7ee0bb Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 17:11:52 +0800 Subject: [PATCH 14/16] FIx: Organize header --- .clang-tidy | 4 +- .clangd | 22 +- run-clang-tidy.py | 795 +++++++++++++++++++++++++++ src/core/Page.cpp | 10 +- src/core/Page.hpp | 20 +- src/ui/App.cpp | 1 - src/ui/App.hpp | 1 + src/ui/Canvas.cpp | 10 +- src/ui/Canvas.hpp | 4 +- src/ui/ConfigPanel.cpp | 8 +- src/ui/ConfigPanel.hpp | 2 - src/ui/MainFrame.cpp | 15 +- src/ui/MainFrame.hpp | 11 +- src/ui/MenuBar.cpp | 3 +- src/ui/MenuBar.hpp | 2 +- src/ui/ToolBar.cpp | 1 + src/ui/components/SliderWithSpin.cpp | 3 +- src/utils/Compare.cpp | 2 + 18 files changed, 867 insertions(+), 47 deletions(-) create mode 100644 run-clang-tidy.py diff --git a/.clang-tidy b/.clang-tidy index 2fa0ccd..893283d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,4 +1,5 @@ -Checks: -*, +Checks: > + -*, bugprone-undelegated-constructor, bugprone-argument-comment, bugprone-bad-signal-to-kill-thread, @@ -144,7 +145,6 @@ Checks: -*, hicpp-exception-baseclass, hicpp-uppercase-literal-suffix, - cppcoreguidelines-macro-usage, cppcoreguidelines-misleading-capture-default-by-value, cppcoreguidelines-missing-std-forward, cppcoreguidelines-no-suspend-with-lock, diff --git a/.clangd b/.clangd index f37df64..bd3ee8a 100644 --- a/.clangd +++ b/.clangd @@ -1,5 +1,17 @@ -Index: - Background: Build - -CompileFlags: - Add: [-xc++, -Wall, -std=c++20] \ No newline at end of file +Index: + Background: Build + +CompileFlags: + Add: [-xc++, -Wall, -std=c++20] + +Diagnostics: + UnusedIncludes: Strict + MissingIncludes: Strict + ClangTidy: + Add: [] + Remove: [] + + Includes: + IgnoreHeader: + - "wx/.*" + - "opencv2/.*" diff --git a/run-clang-tidy.py b/run-clang-tidy.py new file mode 100644 index 0000000..59523fd --- /dev/null +++ b/run-clang-tidy.py @@ -0,0 +1,795 @@ +#!/usr/bin/env python3 +# +# ===-----------------------------------------------------------------------===# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===-----------------------------------------------------------------------===# +# FIXME: Integrate with clang-tidy-diff.py + + +""" +Parallel clang-tidy runner +========================== + +Runs clang-tidy over all files in a compilation database. Requires clang-tidy +and clang-apply-replacements in $PATH. + +Example invocations. +- Run clang-tidy on all files in the current working directory with a default + set of checks and show warnings in the cpp files and all project headers. + run-clang-tidy.py $PWD + +- Fix all header guards. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard + +- Fix all header guards included from clang-tidy and header guards + for clang-tidy headers. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ + -header-filter=extra/clang-tidy + +Compilation database setup: +https://clang.llvm.org/docs/HowToSetupToolingForLLVM.html +""" + +import argparse +import asyncio +from dataclasses import dataclass +import glob +import json +import multiprocessing +import os +import re +import shutil +import subprocess +import sys +import tempfile +import time +import traceback +from types import ModuleType +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, TypeVar + + +yaml: Optional[ModuleType] = None +try: + import yaml +except ImportError: + yaml = None + + +def strtobool(val: str) -> bool: + """Convert a string representation of truth to a bool following LLVM's CLI argument parsing.""" + + val = val.lower() + if val in ["", "true", "1"]: + return True + elif val in ["false", "0"]: + return False + + # Return ArgumentTypeError so that argparse does not substitute its own error message + raise argparse.ArgumentTypeError( + f"'{val}' is invalid value for boolean argument! Try 0 or 1." + ) + + +def find_compilation_database(path: str) -> str: + """Adjusts the directory until a compilation database is found.""" + result = os.path.realpath("./") + while not os.path.isfile(os.path.join(result, path)): + parent = os.path.dirname(result) + if result == parent: + print("Error: could not find compilation database.") + sys.exit(1) + result = parent + return result + + +def get_tidy_invocation( + f: Optional[str], + clang_tidy_binary: str, + checks: str, + tmpdir: Optional[str], + build_path: str, + header_filter: Optional[str], + allow_enabling_alpha_checkers: bool, + extra_arg: List[str], + extra_arg_before: List[str], + removed_arg: List[str], + quiet: bool, + config_file_path: str, + config: str, + line_filter: Optional[str], + use_color: bool, + plugins: List[str], + warnings_as_errors: Optional[str], + exclude_header_filter: Optional[str], + allow_no_checks: bool, + store_check_profile: Optional[str], +) -> List[str]: + """Gets a command line for clang-tidy.""" + start = [clang_tidy_binary] + if allow_enabling_alpha_checkers: + start.append("-allow-enabling-analyzer-alpha-checkers") + if exclude_header_filter is not None: + start.append(f"--exclude-header-filter={exclude_header_filter}") + if header_filter is not None: + start.append(f"-header-filter={header_filter}") + if line_filter is not None: + start.append(f"-line-filter={line_filter}") + if use_color is not None: + if use_color: + start.append("--use-color") + else: + start.append("--use-color=false") + if checks: + start.append(f"-checks={checks}") + if tmpdir is not None: + start.append("-export-fixes") + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir) + os.close(handle) + start.append(name) + for arg in extra_arg: + start.append(f"-extra-arg={arg}") + for arg in extra_arg_before: + start.append(f"-extra-arg-before={arg}") + for arg in removed_arg: + start.append(f"-removed-arg={arg}") + start.append(f"-p={build_path}") + if quiet: + start.append("-quiet") + if config_file_path: + start.append(f"--config-file={config_file_path}") + elif config: + start.append(f"-config={config}") + for plugin in plugins: + start.append(f"-load={plugin}") + if warnings_as_errors: + start.append(f"--warnings-as-errors={warnings_as_errors}") + if allow_no_checks: + start.append("--allow-no-checks") + if store_check_profile: + start.append("--enable-check-profile") + start.append(f"--store-check-profile={store_check_profile}") + if f: + start.append(f) + return start + + +def merge_replacement_files(tmpdir: str, mergefile: str) -> None: + """Merge all replacement files in a directory into a single file""" + assert yaml + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = "Diagnostics" + merged = [] + for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")): + content = yaml.safe_load(open(replacefile, "r")) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {"MainSourceFile": "", mergekey: merged} + with open(mergefile, "w") as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, "w").close() + + +def aggregate_profiles(profile_dir: str) -> Dict[str, float]: + """Aggregate timing data from multiple profile JSON files""" + aggregated: Dict[str, float] = {} + + for profile_file in glob.iglob(os.path.join(profile_dir, "*.json")): + try: + with open(profile_file, "r", encoding="utf-8") as f: + data = json.load(f) + profile_data: Dict[str, float] = data.get("profile", {}) + + for key, value in profile_data.items(): + if key.startswith("time.clang-tidy."): + if key in aggregated: + aggregated[key] += value + else: + aggregated[key] = value + except (json.JSONDecodeError, KeyError, IOError) as e: + print(f"Error: invalid json file {profile_file}: {e}", file=sys.stderr) + continue + + return aggregated + + +def print_profile_data(aggregated_data: Dict[str, float]) -> None: + """Print aggregated checks profile data in the same format as clang-tidy""" + if not aggregated_data: + return + + # Extract checker names and their timing data + checkers: Dict[str, Dict[str, float]] = {} + for key, value in aggregated_data.items(): + parts = key.split(".") + if len(parts) >= 4 and parts[0] == "time" and parts[1] == "clang-tidy": + checker_name = ".".join( + parts[2:-1] + ) # Everything between "clang-tidy" and the timing type + timing_type = parts[-1] # wall, user, or sys + + if checker_name not in checkers: + checkers[checker_name] = {"wall": 0.0, "user": 0.0, "sys": 0.0} + + checkers[checker_name][timing_type] = value + + if not checkers: + return + + total_user = sum(data["user"] for data in checkers.values()) + total_sys = sum(data["sys"] for data in checkers.values()) + total_wall = sum(data["wall"] for data in checkers.values()) + + sorted_checkers: List[Tuple[str, Dict[str, float]]] = sorted( + checkers.items(), key=lambda x: x[1]["user"] + x[1]["sys"], reverse=True + ) + + def print_stderr(*args: Any, **kwargs: Any) -> None: + print(*args, file=sys.stderr, **kwargs) + + print_stderr( + "===-------------------------------------------------------------------------===" + ) + print_stderr(" clang-tidy checks profiling") + print_stderr( + "===-------------------------------------------------------------------------===" + ) + print_stderr( + f" Total Execution Time: {total_user + total_sys:.4f} seconds ({total_wall:.4f} wall clock)\n" + ) + + # Calculate field widths based on the Total line which has the largest values + total_combined = total_user + total_sys + user_width = len(f"{total_user:.4f}") + sys_width = len(f"{total_sys:.4f}") + combined_width = len(f"{total_combined:.4f}") + wall_width = len(f"{total_wall:.4f}") + + # Header with proper alignment + additional_width = 9 # for " (100.0%)" + user_header = "---User Time---".center(user_width + additional_width) + sys_header = "--System Time--".center(sys_width + additional_width) + combined_header = "--User+System--".center(combined_width + additional_width) + wall_header = "---Wall Time---".center(wall_width + additional_width) + + print_stderr( + f" {user_header} {sys_header} {combined_header} {wall_header} --- Name ---" + ) + + for checker_name, data in sorted_checkers: + user_time = data["user"] + sys_time = data["sys"] + wall_time = data["wall"] + combined_time = user_time + sys_time + + user_percent = (user_time / total_user * 100) if total_user > 0 else 0 + sys_percent = (sys_time / total_sys * 100) if total_sys > 0 else 0 + combined_percent = ( + (combined_time / total_combined * 100) if total_combined > 0 else 0 + ) + wall_percent = (wall_time / total_wall * 100) if total_wall > 0 else 0 + + user_str = f"{user_time:{user_width}.4f} ({user_percent:5.1f}%)" + sys_str = f"{sys_time:{sys_width}.4f} ({sys_percent:5.1f}%)" + combined_str = f"{combined_time:{combined_width}.4f} ({combined_percent:5.1f}%)" + wall_str = f"{wall_time:{wall_width}.4f} ({wall_percent:5.1f}%)" + + print_stderr( + f" {user_str} {sys_str} {combined_str} {wall_str} {checker_name}" + ) + + user_total_str = f"{total_user:{user_width}.4f} (100.0%)" + sys_total_str = f"{total_sys:{sys_width}.4f} (100.0%)" + combined_total_str = f"{total_combined:{combined_width}.4f} (100.0%)" + wall_total_str = f"{total_wall:{wall_width}.4f} (100.0%)" + + print_stderr( + f" {user_total_str} {sys_total_str} {combined_total_str} {wall_total_str} Total" + ) + + +def find_binary(arg: str, name: str, build_path: str) -> str: + """Get the path for a binary or exit""" + if arg: + if shutil.which(arg): + return arg + else: + raise SystemExit( + f"error: passed binary '{arg}' was not found or is not executable" + ) + + built_path = os.path.join(build_path, "bin", name) + binary = shutil.which(name) or shutil.which(built_path) + if binary: + return binary + else: + raise SystemExit(f"error: failed to find {name} in $PATH or at {built_path}") + + +def apply_fixes( + args: argparse.Namespace, clang_apply_replacements_binary: str, tmpdir: str +) -> None: + """Calls clang-apply-fixes on a given directory.""" + invocation = [clang_apply_replacements_binary] + invocation.append("-ignore-insert-conflict") + if args.format: + invocation.append("-format") + if args.style: + invocation.append(f"-style={args.style}") + invocation.append(tmpdir) + subprocess.call(invocation) + + +# FIXME Python 3.12: This can be simplified out with run_with_semaphore[T](...). +T = TypeVar("T") + + +async def run_with_semaphore( + semaphore: asyncio.Semaphore, + f: Callable[..., Awaitable[T]], + *args: Any, + **kwargs: Any, +) -> T: + async with semaphore: + return await f(*args, **kwargs) + + +@dataclass +class ClangTidyResult: + filename: str + invocation: List[str] + returncode: int + stdout: str + stderr: str + elapsed: float + + +async def run_tidy( + args: argparse.Namespace, + name: str, + clang_tidy_binary: str, + tmpdir: str, + build_path: str, + store_check_profile: Optional[str], +) -> ClangTidyResult: + """ + Runs clang-tidy on a single file and returns the result. + """ + invocation = get_tidy_invocation( + name, + clang_tidy_binary, + args.checks, + tmpdir, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.removed_arg, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + args.warnings_as_errors, + args.exclude_header_filter, + args.allow_no_checks, + store_check_profile, + ) + + try: + process = await asyncio.create_subprocess_exec( + *invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + start = time.time() + stdout, stderr = await process.communicate() + end = time.time() + except asyncio.CancelledError: + process.terminate() + await process.wait() + raise + + assert process.returncode is not None + return ClangTidyResult( + name, + invocation, + process.returncode, + stdout.decode("UTF-8"), + stderr.decode("UTF-8"), + end - start, + ) + + +async def main() -> None: + parser = argparse.ArgumentParser( + description="Runs clang-tidy over all files " + "in a compilation database. Requires " + "clang-tidy and clang-apply-replacements in " + "$PATH or in your build directory." + ) + parser.add_argument( + "-allow-enabling-alpha-checkers", + action="store_true", + help="Allow alpha checkers from clang-analyzer.", + ) + parser.add_argument( + "-clang-tidy-binary", metavar="PATH", help="Path to clang-tidy binary." + ) + parser.add_argument( + "-clang-apply-replacements-binary", + metavar="PATH", + help="Path to clang-apply-replacements binary.", + ) + parser.add_argument( + "-checks", + default=None, + help="Checks filter, when not specified, use clang-tidy default.", + ) + config_group = parser.add_mutually_exclusive_group() + config_group.add_argument( + "-config", + default=None, + help="Specifies a configuration in YAML/JSON format: " + " -config=\"{Checks: '*', " + ' CheckOptions: {x: y}}" ' + "When the value is empty, clang-tidy will " + "attempt to find a file named .clang-tidy for " + "each source file in its parent directories.", + ) + config_group.add_argument( + "-config-file", + default=None, + help="Specify the path of .clang-tidy or custom config " + "file: e.g. -config-file=/some/path/myTidyConfigFile. " + "This option internally works exactly the same way as " + "-config option after reading specified config file. " + "Use either -config-file or -config, not both.", + ) + parser.add_argument( + "-exclude-header-filter", + default=None, + help="Regular expression matching the names of the " + "headers to exclude diagnostics from. Diagnostics from " + "the main file of each translation unit are always " + "displayed.", + ) + parser.add_argument( + "-header-filter", + default=None, + help="Regular expression matching the names of the " + "headers to output diagnostics from. Diagnostics from " + "the main file of each translation unit are always " + "displayed.", + ) + parser.add_argument( + "-source-filter", + default=None, + help="Regular expression matching the names of the " + "source files from compilation database to output " + "diagnostics from.", + ) + parser.add_argument( + "-line-filter", + default=None, + help="List of files and line ranges to output diagnostics from.", + ) + if yaml: + parser.add_argument( + "-export-fixes", + metavar="file_or_directory", + dest="export_fixes", + help="A directory or a yaml file to store suggested fixes in, " + "which can be applied with clang-apply-replacements. If the " + "parameter is a directory, the fixes of each compilation unit are " + "stored in individual yaml files in the directory.", + ) + else: + parser.add_argument( + "-export-fixes", + metavar="directory", + dest="export_fixes", + help="A directory to store suggested fixes in, which can be applied " + "with clang-apply-replacements. The fixes of each compilation unit are " + "stored in individual yaml files in the directory.", + ) + parser.add_argument( + "-j", + type=int, + default=0, + help="Number of tidy instances to be run in parallel.", + ) + parser.add_argument( + "files", + nargs="*", + default=[".*"], + help="Files to be processed (regex on path).", + ) + parser.add_argument("-fix", action="store_true", help="apply fix-its.") + parser.add_argument( + "-format", action="store_true", help="Reformat code after applying fixes." + ) + parser.add_argument( + "-style", + default="file", + help="The style of reformat code after applying fixes.", + ) + parser.add_argument( + "-use-color", + type=strtobool, + nargs="?", + const=True, + help="Use colors in diagnostics, overriding clang-tidy's" + " default behavior. This option overrides the 'UseColor" + "' option in .clang-tidy file, if any.", + ) + parser.add_argument( + "-p", dest="build_path", help="Path used to read a compile command database." + ) + parser.add_argument( + "-extra-arg", + dest="extra_arg", + action="append", + default=[], + help="Additional argument to append to the compiler command line.", + ) + parser.add_argument( + "-extra-arg-before", + dest="extra_arg_before", + action="append", + default=[], + help="Additional argument to prepend to the compiler command line.", + ) + parser.add_argument( + "-removed-arg", + dest="removed_arg", + action="append", + default=[], + help="Arguments to remove from the compiler command line.", + ) + parser.add_argument( + "-quiet", action="store_true", help="Run clang-tidy in quiet mode." + ) + parser.add_argument( + "-load", + dest="plugins", + action="append", + default=[], + help="Load the specified plugin in clang-tidy.", + ) + parser.add_argument( + "-warnings-as-errors", + default=None, + help="Upgrades warnings to errors. Same format as '-checks'.", + ) + parser.add_argument( + "-allow-no-checks", + action="store_true", + help="Allow empty enabled checks.", + ) + parser.add_argument( + "-enable-check-profile", + action="store_true", + help="Enable per-check timing profiles, and print a report", + ) + parser.add_argument( + "-hide-progress", + action="store_true", + help="Hide progress", + ) + args = parser.parse_args() + + db_path = "compile_commands.json" + + if args.build_path is not None: + build_path = args.build_path + else: + # Find our database + build_path = find_compilation_database(db_path) + + clang_tidy_binary = find_binary(args.clang_tidy_binary, "clang-tidy", build_path) + + if args.fix: + clang_apply_replacements_binary = find_binary( + args.clang_apply_replacements_binary, "clang-apply-replacements", build_path + ) + + combine_fixes = False + export_fixes_dir: Optional[str] = None + delete_fixes_dir = False + if args.export_fixes is not None: + # if a directory is given, create it if it does not exist + if args.export_fixes.endswith(os.path.sep) and not os.path.isdir( + args.export_fixes + ): + os.makedirs(args.export_fixes) + + if not os.path.isdir(args.export_fixes): + if not yaml: + raise RuntimeError( + "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory." + ) + + combine_fixes = True + + if os.path.isdir(args.export_fixes): + export_fixes_dir = args.export_fixes + + if export_fixes_dir is None and (args.fix or combine_fixes): + export_fixes_dir = tempfile.mkdtemp() + delete_fixes_dir = True + + profile_dir: Optional[str] = None + if args.enable_check_profile: + profile_dir = tempfile.mkdtemp() + + try: + invocation = get_tidy_invocation( + None, + clang_tidy_binary, + args.checks, + None, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.removed_arg, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + args.warnings_as_errors, + args.exclude_header_filter, + args.allow_no_checks, + None, # No profiling for the list-checks invocation + ) + invocation.append("-list-checks") + invocation.append("-") + # Even with -quiet we still want to check if we can call clang-tidy. + subprocess.check_call( + invocation, stdout=subprocess.DEVNULL if args.quiet else None + ) + except: + print("Unable to run clang-tidy.", file=sys.stderr) + sys.exit(1) + + # Load the database and extract all files. + with open(os.path.join(build_path, db_path)) as f: + database = json.load(f) + files = {os.path.abspath(os.path.join(e["directory"], e["file"])) for e in database} + number_files_in_database = len(files) + + # Filter source files from compilation database. + if args.source_filter: + try: + source_filter_re = re.compile(args.source_filter) + except: + print( + "Error: unable to compile regex from arg -source-filter:", + file=sys.stderr, + ) + traceback.print_exc() + sys.exit(1) + files = {f for f in files if source_filter_re.match(f)} + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() + + # Build up a big regexy filter from all command line arguments. + file_name_re = re.compile("|".join(args.files)) + files = {f for f in files if file_name_re.search(f)} + + if not args.hide_progress: + print( + f"Running clang-tidy in {max_task} threads for {len(files)} files " + f"out of {number_files_in_database} in compilation database ..." + ) + + returncode = 0 + semaphore = asyncio.Semaphore(max_task) + tasks = [ + asyncio.create_task( + run_with_semaphore( + semaphore, + run_tidy, + args, + f, + clang_tidy_binary, + export_fixes_dir, + build_path, + profile_dir, + ) + ) + for f in files + ] + + try: + for i, coro in enumerate(asyncio.as_completed(tasks)): + result = await coro + if result.returncode != 0: + returncode = 1 + if result.returncode < 0: + result.stderr += f"{result.filename}: terminated by signal {-result.returncode}\n" + progress = f"[{i + 1: >{len(f'{len(files)}')}}/{len(files)}]" + runtime = f"[{result.elapsed:.1f}s]" + if not args.hide_progress: + print(f"{progress}{runtime} {' '.join(result.invocation)}") + if result.stdout: + print(result.stdout, end=("" if result.stderr else "\n")) + if result.stderr: + print(result.stderr) + except asyncio.CancelledError: + if not args.hide_progress: + print("\nCtrl-C detected, goodbye.") + for task in tasks: + task.cancel() + if delete_fixes_dir: + assert export_fixes_dir + shutil.rmtree(export_fixes_dir) + if profile_dir: + shutil.rmtree(profile_dir) + return + + if args.enable_check_profile and profile_dir: + # Ensure all clang-tidy stdout is flushed before printing profiling + sys.stdout.flush() + aggregated_data = aggregate_profiles(profile_dir) + if aggregated_data: + print_profile_data(aggregated_data) + else: + print("No profiling data found.") + + if combine_fixes: + if not args.hide_progress: + print(f"Writing fixes to {args.export_fixes} ...") + try: + assert export_fixes_dir + merge_replacement_files(export_fixes_dir, args.export_fixes) + except: + print("Error exporting fixes.\n", file=sys.stderr) + traceback.print_exc() + returncode = 1 + + if args.fix: + if not args.hide_progress: + print("Applying fixes ...") + try: + assert export_fixes_dir + apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir) + except: + print("Error applying fixes.\n", file=sys.stderr) + traceback.print_exc() + returncode = 1 + + if delete_fixes_dir: + assert export_fixes_dir + shutil.rmtree(export_fixes_dir) + if profile_dir: + shutil.rmtree(profile_dir) + sys.exit(returncode) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass diff --git a/src/core/Page.cpp b/src/core/Page.cpp index 561dbda..9c66081 100644 --- a/src/core/Page.cpp +++ b/src/core/Page.cpp @@ -1,7 +1,14 @@ #include "core/Page.hpp" +#include +#include +#include +#include +#include +#include #include #include +#include #include @@ -100,7 +107,8 @@ static std::optional CalculateSelectArea(const cv::Mat& image, int filte cv::threshold(img_dst, img_dst, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); std::vector> contours; cv::findContours(img_dst, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); - int x_min = INT_MAX, x_max = INT_MIN, y_min = INT_MAX, y_max = INT_MIN; + int x_min = std::numeric_limits::max(), x_max = std::numeric_limits::min(), + y_min = std::numeric_limits::max(), y_max = std::numeric_limits::min(); bool has_point = false; for (const auto& contour : contours) { if (cv::contourArea(contour) >= filter_noise_size) { diff --git a/src/core/Page.hpp b/src/core/Page.hpp index a93ef13..ed1e0ab 100644 --- a/src/core/Page.hpp +++ b/src/core/Page.hpp @@ -5,9 +5,7 @@ #include #include -#include -#include -#include +#include #include "core/DocumentData.hpp" @@ -19,11 +17,11 @@ class Page { public: ~Page() { m_image.Destroy(); } - Document& GetDocument() const { return m_doc; } - DocumentConfig& GetConfig() const; - const std::filesystem::path& GetImagePath() const { return m_pageData.path; } - const std::set& GetCropLines() const { return m_pageData.crop_lines; } - wxImage& GetImage() { return m_image; } + [[nodiscard]] Document& GetDocument() const { return m_doc; } + [[nodiscard]] DocumentConfig& GetConfig() const; + [[nodiscard]] const std::filesystem::path& GetImagePath() const { return m_pageData.path; } + [[nodiscard]] const std::set& GetCropLines() const { return m_pageData.crop_lines; } + wxImage& GetImage() { return m_image; } const std::vector& getSelectAreas() { if (m_modified) CalculateSelectAreas(); @@ -35,12 +33,12 @@ class Page { bool SaveCrops(); - std::optional SearchNearestLine(int searchPosition, int threshold) const; + [[nodiscard]] std::optional SearchNearestLine(int searchPosition, int threshold) const; private: friend class Document; - Page(Document& doc, PageData& data) : m_doc(doc), m_pageData(data) { LoadImage(); } + Page(Document& doc, PageData& data) : m_doc(doc), m_pageData(data) { LoadImageFromFile(); } bool m_modified = true; // 用来提示 CalculateSelectAreas 的惰性求值 @@ -50,7 +48,7 @@ class Page { std::vector m_selectAreas; wxImage m_image; - void LoadImage() { m_image.LoadFile(wxString(GetImagePath())); } + void LoadImageFromFile() { m_image.LoadFile(wxString(GetImagePath())); } void CalculateSelectAreas(); }; } // namespace croplines diff --git a/src/ui/App.cpp b/src/ui/App.cpp index e60b414..d4e3f55 100644 --- a/src/ui/App.cpp +++ b/src/ui/App.cpp @@ -1,6 +1,5 @@ #include "ui/App.hpp" -#include #include #include "ui/MainFrame.hpp" diff --git a/src/ui/App.hpp b/src/ui/App.hpp index c491aeb..f2dcda5 100644 --- a/src/ui/App.hpp +++ b/src/ui/App.hpp @@ -1,4 +1,5 @@ #pragma once + #include #include "ui/MainFrame.hpp" diff --git a/src/ui/Canvas.cpp b/src/ui/Canvas.cpp index 0a3bebd..df3ff6a 100644 --- a/src/ui/Canvas.cpp +++ b/src/ui/Canvas.cpp @@ -1,8 +1,16 @@ #include "ui/Canvas.hpp" +#include #include +#include -#include +#include +#include +// Include wx before gl +#include + +#include "core/ImageScaleModel.hpp" +#include "core/Page.hpp" using namespace croplines; diff --git a/src/ui/Canvas.hpp b/src/ui/Canvas.hpp index fc44af8..291a62b 100644 --- a/src/ui/Canvas.hpp +++ b/src/ui/Canvas.hpp @@ -4,9 +4,11 @@ #include #include -#include #include #include +// include wx before gl +#include + #include "core/Document.hpp" #include "core/ImageScaleModel.hpp" diff --git a/src/ui/ConfigPanel.cpp b/src/ui/ConfigPanel.cpp index 5b99b0a..4224d8a 100644 --- a/src/ui/ConfigPanel.cpp +++ b/src/ui/ConfigPanel.cpp @@ -1,14 +1,10 @@ #include "ui/ConfigPanel.hpp" -#include -#include -#include -#include +#include #include -#include -#include #include "core/DocumentData.hpp" +#include "ui/components/SliderWithSpin.hpp" #include "ui/Defs.hpp" using namespace croplines; diff --git a/src/ui/ConfigPanel.hpp b/src/ui/ConfigPanel.hpp index 75d7270..ac7384c 100644 --- a/src/ui/ConfigPanel.hpp +++ b/src/ui/ConfigPanel.hpp @@ -1,8 +1,6 @@ #pragma once -#include #include -#include #include #include "core/DocumentData.hpp" diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp index cc057d7..09dc2a0 100644 --- a/src/ui/MainFrame.cpp +++ b/src/ui/MainFrame.cpp @@ -1,20 +1,23 @@ #include "ui/MainFrame.hpp" +#include +#include +#include +#include +#include #include #include +#include // #include #include #include -#include -#include -#include -#include -#include -#include +#include #include "core/Document.hpp" +#include "core/DocumentData.hpp" +#include "core/Page.hpp" #include "ui/Canvas.hpp" #include "ui/components/SliderWithSpin.hpp" #include "ui/ConfigPanel.hpp" diff --git a/src/ui/MainFrame.hpp b/src/ui/MainFrame.hpp index b65cab1..f4c0e40 100644 --- a/src/ui/MainFrame.hpp +++ b/src/ui/MainFrame.hpp @@ -1,15 +1,14 @@ #pragma once +#include +#include +#include + #include -#include -#include -#include -#include -#include -#include #include #include "core/Document.hpp" +#include "core/Page.hpp" #include "ui/Canvas.hpp" #include "ui/ConfigPanel.hpp" #include "ui/MenuBar.hpp" diff --git a/src/ui/MenuBar.cpp b/src/ui/MenuBar.cpp index 8ee22c2..dd48d45 100644 --- a/src/ui/MenuBar.cpp +++ b/src/ui/MenuBar.cpp @@ -1,7 +1,6 @@ #include "ui/MenuBar.hpp" -#include -#include +#include #include "ui/Defs.hpp" diff --git a/src/ui/MenuBar.hpp b/src/ui/MenuBar.hpp index 081705b..de8ec93 100644 --- a/src/ui/MenuBar.hpp +++ b/src/ui/MenuBar.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include // IWYU pragma: keep namespace croplines { class MenuBar : public wxMenuBar { diff --git a/src/ui/ToolBar.cpp b/src/ui/ToolBar.cpp index 4a3f7ea..c8f402e 100644 --- a/src/ui/ToolBar.cpp +++ b/src/ui/ToolBar.cpp @@ -1,6 +1,7 @@ #include "ui/ToolBar.hpp" #include +#include #include "ui/Defs.hpp" diff --git a/src/ui/components/SliderWithSpin.cpp b/src/ui/components/SliderWithSpin.cpp index e8b7a69..c63e822 100644 --- a/src/ui/components/SliderWithSpin.cpp +++ b/src/ui/components/SliderWithSpin.cpp @@ -1,7 +1,6 @@ #include "ui/components/SliderWithSpin.hpp" -#include -#include +#include using namespace croplines; diff --git a/src/utils/Compare.cpp b/src/utils/Compare.cpp index 6d988ec..4b2c80b 100644 --- a/src/utils/Compare.cpp +++ b/src/utils/Compare.cpp @@ -1,7 +1,9 @@ #include "utils/Compare.hpp" #include +#include #include +#include std::strong_ordering croplines::NaturalCompare(std::string_view a, std::string_view b) { for (const char *i1 = a.begin(), *i2 = b.begin(); i1 != a.end() && i2 != b.end(); ++i1, ++i2) { From 0d605d287805fe03db7efd70291a23c07a57c9e4 Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 17:21:09 +0800 Subject: [PATCH 15/16] Fix: windres.exe: can't open cursor file `wx/msw/hand.cur': Invalid argument --- CMakeLists.txt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e5935c..5a2742a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,16 +13,14 @@ endif() if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) set(wxWidgets_USE_DEBUG ON) else() - if(MSVC OR WIN32) - set(wxWidgets_USE_STATIC ON) - endif() + set(wxWidgets_USE_STATIC ON) endif() find_package(wxWidgets REQUIRED aui core base gl) find_package(Eigen3 REQUIRED) # required by opencv find_package(OpenCV REQUIRED) find_package(OpenMP REQUIRED) -find_package(cereal REQUIRED) include(${wxWidgets_USE_FILE}) +include_directories(${OpenCV_INCLUDE_DIRS}) file(GLOB_RECURSE SRCS src/*.cpp) if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") @@ -35,12 +33,8 @@ else() add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) endif() target_include_directories(Croplines PRIVATE src) -target_include_directories(Croplines PRIVATE cereal::cereal) target_link_libraries(Croplines ${wxWidgets_LIBRARIES}) -# OpenCV target_link_libraries(Croplines ${OpenCV_LIBS}) -target_include_directories(Croplines PRIVATE ${OpenCV_INCLUDE_DIRS}) -# OpenGL target_link_libraries(Croplines opengl32) From 394d9e78809fbee5fef3eed2996b944e0d0a261f Mon Sep 17 00:00:00 2001 From: Likend Date: Thu, 8 Jan 2026 18:41:18 +0800 Subject: [PATCH 16/16] CI: Include cereal --- .clang-tidy | 4 +- CMakeLists.txt | 10 +- run-clang-tidy.py | 795 ------------------------------------------- src/ui/MainFrame.cpp | 4 +- 4 files changed, 10 insertions(+), 803 deletions(-) delete mode 100644 run-clang-tidy.py diff --git a/.clang-tidy b/.clang-tidy index 893283d..f9a4465 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,3 +1,5 @@ +HeaderFilterRegex: 'src/.*' + Checks: > -*, bugprone-undelegated-constructor, @@ -97,10 +99,8 @@ Checks: > modernize-use-uncaught-exceptions, modernize-use-using, - misc-const-correctness, misc-definitions-in-headers, misc-header-include-cycle, - misc-include-cleaner, misc-misplaced-const, misc-redundant-expression, misc-static-assert, diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a2742a..81d360b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,8 +19,8 @@ find_package(wxWidgets REQUIRED aui core base gl) find_package(Eigen3 REQUIRED) # required by opencv find_package(OpenCV REQUIRED) find_package(OpenMP REQUIRED) +find_package(cereal REQUIRED) include(${wxWidgets_USE_FILE}) -include_directories(${OpenCV_INCLUDE_DIRS}) file(GLOB_RECURSE SRCS src/*.cpp) if (${CMAKE_SYSTEM_NAME} MATCHES "Windows") @@ -33,9 +33,11 @@ else() add_executable(Croplines WIN32 ${SRCS} ${WIN32_RESOURCES}) endif() target_include_directories(Croplines PRIVATE src) -target_link_libraries(Croplines ${wxWidgets_LIBRARIES}) -target_link_libraries(Croplines ${OpenCV_LIBS}) -target_link_libraries(Croplines opengl32) +target_include_directories(Croplines PRIVATE ${OpenCV_INCLUDE_DIRS}) +target_link_libraries(Croplines PRIVATE ${wxWidgets_LIBRARIES}) +target_link_libraries(Croplines PRIVATE ${OpenCV_LIBS}) +target_link_libraries(Croplines PRIVATE opengl32) +target_link_libraries(Croplines PRIVATE cereal::cereal) if(CMAKE_BUILD_TYPE AND (CMAKE_BUILD_TYPE STREQUAL "Release")) diff --git a/run-clang-tidy.py b/run-clang-tidy.py deleted file mode 100644 index 59523fd..0000000 --- a/run-clang-tidy.py +++ /dev/null @@ -1,795 +0,0 @@ -#!/usr/bin/env python3 -# -# ===-----------------------------------------------------------------------===# -# -# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. -# See https://llvm.org/LICENSE.txt for license information. -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -# -# ===-----------------------------------------------------------------------===# -# FIXME: Integrate with clang-tidy-diff.py - - -""" -Parallel clang-tidy runner -========================== - -Runs clang-tidy over all files in a compilation database. Requires clang-tidy -and clang-apply-replacements in $PATH. - -Example invocations. -- Run clang-tidy on all files in the current working directory with a default - set of checks and show warnings in the cpp files and all project headers. - run-clang-tidy.py $PWD - -- Fix all header guards. - run-clang-tidy.py -fix -checks=-*,llvm-header-guard - -- Fix all header guards included from clang-tidy and header guards - for clang-tidy headers. - run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ - -header-filter=extra/clang-tidy - -Compilation database setup: -https://clang.llvm.org/docs/HowToSetupToolingForLLVM.html -""" - -import argparse -import asyncio -from dataclasses import dataclass -import glob -import json -import multiprocessing -import os -import re -import shutil -import subprocess -import sys -import tempfile -import time -import traceback -from types import ModuleType -from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, TypeVar - - -yaml: Optional[ModuleType] = None -try: - import yaml -except ImportError: - yaml = None - - -def strtobool(val: str) -> bool: - """Convert a string representation of truth to a bool following LLVM's CLI argument parsing.""" - - val = val.lower() - if val in ["", "true", "1"]: - return True - elif val in ["false", "0"]: - return False - - # Return ArgumentTypeError so that argparse does not substitute its own error message - raise argparse.ArgumentTypeError( - f"'{val}' is invalid value for boolean argument! Try 0 or 1." - ) - - -def find_compilation_database(path: str) -> str: - """Adjusts the directory until a compilation database is found.""" - result = os.path.realpath("./") - while not os.path.isfile(os.path.join(result, path)): - parent = os.path.dirname(result) - if result == parent: - print("Error: could not find compilation database.") - sys.exit(1) - result = parent - return result - - -def get_tidy_invocation( - f: Optional[str], - clang_tidy_binary: str, - checks: str, - tmpdir: Optional[str], - build_path: str, - header_filter: Optional[str], - allow_enabling_alpha_checkers: bool, - extra_arg: List[str], - extra_arg_before: List[str], - removed_arg: List[str], - quiet: bool, - config_file_path: str, - config: str, - line_filter: Optional[str], - use_color: bool, - plugins: List[str], - warnings_as_errors: Optional[str], - exclude_header_filter: Optional[str], - allow_no_checks: bool, - store_check_profile: Optional[str], -) -> List[str]: - """Gets a command line for clang-tidy.""" - start = [clang_tidy_binary] - if allow_enabling_alpha_checkers: - start.append("-allow-enabling-analyzer-alpha-checkers") - if exclude_header_filter is not None: - start.append(f"--exclude-header-filter={exclude_header_filter}") - if header_filter is not None: - start.append(f"-header-filter={header_filter}") - if line_filter is not None: - start.append(f"-line-filter={line_filter}") - if use_color is not None: - if use_color: - start.append("--use-color") - else: - start.append("--use-color=false") - if checks: - start.append(f"-checks={checks}") - if tmpdir is not None: - start.append("-export-fixes") - # Get a temporary file. We immediately close the handle so clang-tidy can - # overwrite it. - (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir) - os.close(handle) - start.append(name) - for arg in extra_arg: - start.append(f"-extra-arg={arg}") - for arg in extra_arg_before: - start.append(f"-extra-arg-before={arg}") - for arg in removed_arg: - start.append(f"-removed-arg={arg}") - start.append(f"-p={build_path}") - if quiet: - start.append("-quiet") - if config_file_path: - start.append(f"--config-file={config_file_path}") - elif config: - start.append(f"-config={config}") - for plugin in plugins: - start.append(f"-load={plugin}") - if warnings_as_errors: - start.append(f"--warnings-as-errors={warnings_as_errors}") - if allow_no_checks: - start.append("--allow-no-checks") - if store_check_profile: - start.append("--enable-check-profile") - start.append(f"--store-check-profile={store_check_profile}") - if f: - start.append(f) - return start - - -def merge_replacement_files(tmpdir: str, mergefile: str) -> None: - """Merge all replacement files in a directory into a single file""" - assert yaml - # The fixes suggested by clang-tidy >= 4.0.0 are given under - # the top level key 'Diagnostics' in the output yaml files - mergekey = "Diagnostics" - merged = [] - for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")): - content = yaml.safe_load(open(replacefile, "r")) - if not content: - continue # Skip empty files. - merged.extend(content.get(mergekey, [])) - - if merged: - # MainSourceFile: The key is required by the definition inside - # include/clang/Tooling/ReplacementsYaml.h, but the value - # is actually never used inside clang-apply-replacements, - # so we set it to '' here. - output = {"MainSourceFile": "", mergekey: merged} - with open(mergefile, "w") as out: - yaml.safe_dump(output, out) - else: - # Empty the file: - open(mergefile, "w").close() - - -def aggregate_profiles(profile_dir: str) -> Dict[str, float]: - """Aggregate timing data from multiple profile JSON files""" - aggregated: Dict[str, float] = {} - - for profile_file in glob.iglob(os.path.join(profile_dir, "*.json")): - try: - with open(profile_file, "r", encoding="utf-8") as f: - data = json.load(f) - profile_data: Dict[str, float] = data.get("profile", {}) - - for key, value in profile_data.items(): - if key.startswith("time.clang-tidy."): - if key in aggregated: - aggregated[key] += value - else: - aggregated[key] = value - except (json.JSONDecodeError, KeyError, IOError) as e: - print(f"Error: invalid json file {profile_file}: {e}", file=sys.stderr) - continue - - return aggregated - - -def print_profile_data(aggregated_data: Dict[str, float]) -> None: - """Print aggregated checks profile data in the same format as clang-tidy""" - if not aggregated_data: - return - - # Extract checker names and their timing data - checkers: Dict[str, Dict[str, float]] = {} - for key, value in aggregated_data.items(): - parts = key.split(".") - if len(parts) >= 4 and parts[0] == "time" and parts[1] == "clang-tidy": - checker_name = ".".join( - parts[2:-1] - ) # Everything between "clang-tidy" and the timing type - timing_type = parts[-1] # wall, user, or sys - - if checker_name not in checkers: - checkers[checker_name] = {"wall": 0.0, "user": 0.0, "sys": 0.0} - - checkers[checker_name][timing_type] = value - - if not checkers: - return - - total_user = sum(data["user"] for data in checkers.values()) - total_sys = sum(data["sys"] for data in checkers.values()) - total_wall = sum(data["wall"] for data in checkers.values()) - - sorted_checkers: List[Tuple[str, Dict[str, float]]] = sorted( - checkers.items(), key=lambda x: x[1]["user"] + x[1]["sys"], reverse=True - ) - - def print_stderr(*args: Any, **kwargs: Any) -> None: - print(*args, file=sys.stderr, **kwargs) - - print_stderr( - "===-------------------------------------------------------------------------===" - ) - print_stderr(" clang-tidy checks profiling") - print_stderr( - "===-------------------------------------------------------------------------===" - ) - print_stderr( - f" Total Execution Time: {total_user + total_sys:.4f} seconds ({total_wall:.4f} wall clock)\n" - ) - - # Calculate field widths based on the Total line which has the largest values - total_combined = total_user + total_sys - user_width = len(f"{total_user:.4f}") - sys_width = len(f"{total_sys:.4f}") - combined_width = len(f"{total_combined:.4f}") - wall_width = len(f"{total_wall:.4f}") - - # Header with proper alignment - additional_width = 9 # for " (100.0%)" - user_header = "---User Time---".center(user_width + additional_width) - sys_header = "--System Time--".center(sys_width + additional_width) - combined_header = "--User+System--".center(combined_width + additional_width) - wall_header = "---Wall Time---".center(wall_width + additional_width) - - print_stderr( - f" {user_header} {sys_header} {combined_header} {wall_header} --- Name ---" - ) - - for checker_name, data in sorted_checkers: - user_time = data["user"] - sys_time = data["sys"] - wall_time = data["wall"] - combined_time = user_time + sys_time - - user_percent = (user_time / total_user * 100) if total_user > 0 else 0 - sys_percent = (sys_time / total_sys * 100) if total_sys > 0 else 0 - combined_percent = ( - (combined_time / total_combined * 100) if total_combined > 0 else 0 - ) - wall_percent = (wall_time / total_wall * 100) if total_wall > 0 else 0 - - user_str = f"{user_time:{user_width}.4f} ({user_percent:5.1f}%)" - sys_str = f"{sys_time:{sys_width}.4f} ({sys_percent:5.1f}%)" - combined_str = f"{combined_time:{combined_width}.4f} ({combined_percent:5.1f}%)" - wall_str = f"{wall_time:{wall_width}.4f} ({wall_percent:5.1f}%)" - - print_stderr( - f" {user_str} {sys_str} {combined_str} {wall_str} {checker_name}" - ) - - user_total_str = f"{total_user:{user_width}.4f} (100.0%)" - sys_total_str = f"{total_sys:{sys_width}.4f} (100.0%)" - combined_total_str = f"{total_combined:{combined_width}.4f} (100.0%)" - wall_total_str = f"{total_wall:{wall_width}.4f} (100.0%)" - - print_stderr( - f" {user_total_str} {sys_total_str} {combined_total_str} {wall_total_str} Total" - ) - - -def find_binary(arg: str, name: str, build_path: str) -> str: - """Get the path for a binary or exit""" - if arg: - if shutil.which(arg): - return arg - else: - raise SystemExit( - f"error: passed binary '{arg}' was not found or is not executable" - ) - - built_path = os.path.join(build_path, "bin", name) - binary = shutil.which(name) or shutil.which(built_path) - if binary: - return binary - else: - raise SystemExit(f"error: failed to find {name} in $PATH or at {built_path}") - - -def apply_fixes( - args: argparse.Namespace, clang_apply_replacements_binary: str, tmpdir: str -) -> None: - """Calls clang-apply-fixes on a given directory.""" - invocation = [clang_apply_replacements_binary] - invocation.append("-ignore-insert-conflict") - if args.format: - invocation.append("-format") - if args.style: - invocation.append(f"-style={args.style}") - invocation.append(tmpdir) - subprocess.call(invocation) - - -# FIXME Python 3.12: This can be simplified out with run_with_semaphore[T](...). -T = TypeVar("T") - - -async def run_with_semaphore( - semaphore: asyncio.Semaphore, - f: Callable[..., Awaitable[T]], - *args: Any, - **kwargs: Any, -) -> T: - async with semaphore: - return await f(*args, **kwargs) - - -@dataclass -class ClangTidyResult: - filename: str - invocation: List[str] - returncode: int - stdout: str - stderr: str - elapsed: float - - -async def run_tidy( - args: argparse.Namespace, - name: str, - clang_tidy_binary: str, - tmpdir: str, - build_path: str, - store_check_profile: Optional[str], -) -> ClangTidyResult: - """ - Runs clang-tidy on a single file and returns the result. - """ - invocation = get_tidy_invocation( - name, - clang_tidy_binary, - args.checks, - tmpdir, - build_path, - args.header_filter, - args.allow_enabling_alpha_checkers, - args.extra_arg, - args.extra_arg_before, - args.removed_arg, - args.quiet, - args.config_file, - args.config, - args.line_filter, - args.use_color, - args.plugins, - args.warnings_as_errors, - args.exclude_header_filter, - args.allow_no_checks, - store_check_profile, - ) - - try: - process = await asyncio.create_subprocess_exec( - *invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - start = time.time() - stdout, stderr = await process.communicate() - end = time.time() - except asyncio.CancelledError: - process.terminate() - await process.wait() - raise - - assert process.returncode is not None - return ClangTidyResult( - name, - invocation, - process.returncode, - stdout.decode("UTF-8"), - stderr.decode("UTF-8"), - end - start, - ) - - -async def main() -> None: - parser = argparse.ArgumentParser( - description="Runs clang-tidy over all files " - "in a compilation database. Requires " - "clang-tidy and clang-apply-replacements in " - "$PATH or in your build directory." - ) - parser.add_argument( - "-allow-enabling-alpha-checkers", - action="store_true", - help="Allow alpha checkers from clang-analyzer.", - ) - parser.add_argument( - "-clang-tidy-binary", metavar="PATH", help="Path to clang-tidy binary." - ) - parser.add_argument( - "-clang-apply-replacements-binary", - metavar="PATH", - help="Path to clang-apply-replacements binary.", - ) - parser.add_argument( - "-checks", - default=None, - help="Checks filter, when not specified, use clang-tidy default.", - ) - config_group = parser.add_mutually_exclusive_group() - config_group.add_argument( - "-config", - default=None, - help="Specifies a configuration in YAML/JSON format: " - " -config=\"{Checks: '*', " - ' CheckOptions: {x: y}}" ' - "When the value is empty, clang-tidy will " - "attempt to find a file named .clang-tidy for " - "each source file in its parent directories.", - ) - config_group.add_argument( - "-config-file", - default=None, - help="Specify the path of .clang-tidy or custom config " - "file: e.g. -config-file=/some/path/myTidyConfigFile. " - "This option internally works exactly the same way as " - "-config option after reading specified config file. " - "Use either -config-file or -config, not both.", - ) - parser.add_argument( - "-exclude-header-filter", - default=None, - help="Regular expression matching the names of the " - "headers to exclude diagnostics from. Diagnostics from " - "the main file of each translation unit are always " - "displayed.", - ) - parser.add_argument( - "-header-filter", - default=None, - help="Regular expression matching the names of the " - "headers to output diagnostics from. Diagnostics from " - "the main file of each translation unit are always " - "displayed.", - ) - parser.add_argument( - "-source-filter", - default=None, - help="Regular expression matching the names of the " - "source files from compilation database to output " - "diagnostics from.", - ) - parser.add_argument( - "-line-filter", - default=None, - help="List of files and line ranges to output diagnostics from.", - ) - if yaml: - parser.add_argument( - "-export-fixes", - metavar="file_or_directory", - dest="export_fixes", - help="A directory or a yaml file to store suggested fixes in, " - "which can be applied with clang-apply-replacements. If the " - "parameter is a directory, the fixes of each compilation unit are " - "stored in individual yaml files in the directory.", - ) - else: - parser.add_argument( - "-export-fixes", - metavar="directory", - dest="export_fixes", - help="A directory to store suggested fixes in, which can be applied " - "with clang-apply-replacements. The fixes of each compilation unit are " - "stored in individual yaml files in the directory.", - ) - parser.add_argument( - "-j", - type=int, - default=0, - help="Number of tidy instances to be run in parallel.", - ) - parser.add_argument( - "files", - nargs="*", - default=[".*"], - help="Files to be processed (regex on path).", - ) - parser.add_argument("-fix", action="store_true", help="apply fix-its.") - parser.add_argument( - "-format", action="store_true", help="Reformat code after applying fixes." - ) - parser.add_argument( - "-style", - default="file", - help="The style of reformat code after applying fixes.", - ) - parser.add_argument( - "-use-color", - type=strtobool, - nargs="?", - const=True, - help="Use colors in diagnostics, overriding clang-tidy's" - " default behavior. This option overrides the 'UseColor" - "' option in .clang-tidy file, if any.", - ) - parser.add_argument( - "-p", dest="build_path", help="Path used to read a compile command database." - ) - parser.add_argument( - "-extra-arg", - dest="extra_arg", - action="append", - default=[], - help="Additional argument to append to the compiler command line.", - ) - parser.add_argument( - "-extra-arg-before", - dest="extra_arg_before", - action="append", - default=[], - help="Additional argument to prepend to the compiler command line.", - ) - parser.add_argument( - "-removed-arg", - dest="removed_arg", - action="append", - default=[], - help="Arguments to remove from the compiler command line.", - ) - parser.add_argument( - "-quiet", action="store_true", help="Run clang-tidy in quiet mode." - ) - parser.add_argument( - "-load", - dest="plugins", - action="append", - default=[], - help="Load the specified plugin in clang-tidy.", - ) - parser.add_argument( - "-warnings-as-errors", - default=None, - help="Upgrades warnings to errors. Same format as '-checks'.", - ) - parser.add_argument( - "-allow-no-checks", - action="store_true", - help="Allow empty enabled checks.", - ) - parser.add_argument( - "-enable-check-profile", - action="store_true", - help="Enable per-check timing profiles, and print a report", - ) - parser.add_argument( - "-hide-progress", - action="store_true", - help="Hide progress", - ) - args = parser.parse_args() - - db_path = "compile_commands.json" - - if args.build_path is not None: - build_path = args.build_path - else: - # Find our database - build_path = find_compilation_database(db_path) - - clang_tidy_binary = find_binary(args.clang_tidy_binary, "clang-tidy", build_path) - - if args.fix: - clang_apply_replacements_binary = find_binary( - args.clang_apply_replacements_binary, "clang-apply-replacements", build_path - ) - - combine_fixes = False - export_fixes_dir: Optional[str] = None - delete_fixes_dir = False - if args.export_fixes is not None: - # if a directory is given, create it if it does not exist - if args.export_fixes.endswith(os.path.sep) and not os.path.isdir( - args.export_fixes - ): - os.makedirs(args.export_fixes) - - if not os.path.isdir(args.export_fixes): - if not yaml: - raise RuntimeError( - "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory." - ) - - combine_fixes = True - - if os.path.isdir(args.export_fixes): - export_fixes_dir = args.export_fixes - - if export_fixes_dir is None and (args.fix or combine_fixes): - export_fixes_dir = tempfile.mkdtemp() - delete_fixes_dir = True - - profile_dir: Optional[str] = None - if args.enable_check_profile: - profile_dir = tempfile.mkdtemp() - - try: - invocation = get_tidy_invocation( - None, - clang_tidy_binary, - args.checks, - None, - build_path, - args.header_filter, - args.allow_enabling_alpha_checkers, - args.extra_arg, - args.extra_arg_before, - args.removed_arg, - args.quiet, - args.config_file, - args.config, - args.line_filter, - args.use_color, - args.plugins, - args.warnings_as_errors, - args.exclude_header_filter, - args.allow_no_checks, - None, # No profiling for the list-checks invocation - ) - invocation.append("-list-checks") - invocation.append("-") - # Even with -quiet we still want to check if we can call clang-tidy. - subprocess.check_call( - invocation, stdout=subprocess.DEVNULL if args.quiet else None - ) - except: - print("Unable to run clang-tidy.", file=sys.stderr) - sys.exit(1) - - # Load the database and extract all files. - with open(os.path.join(build_path, db_path)) as f: - database = json.load(f) - files = {os.path.abspath(os.path.join(e["directory"], e["file"])) for e in database} - number_files_in_database = len(files) - - # Filter source files from compilation database. - if args.source_filter: - try: - source_filter_re = re.compile(args.source_filter) - except: - print( - "Error: unable to compile regex from arg -source-filter:", - file=sys.stderr, - ) - traceback.print_exc() - sys.exit(1) - files = {f for f in files if source_filter_re.match(f)} - - max_task = args.j - if max_task == 0: - max_task = multiprocessing.cpu_count() - - # Build up a big regexy filter from all command line arguments. - file_name_re = re.compile("|".join(args.files)) - files = {f for f in files if file_name_re.search(f)} - - if not args.hide_progress: - print( - f"Running clang-tidy in {max_task} threads for {len(files)} files " - f"out of {number_files_in_database} in compilation database ..." - ) - - returncode = 0 - semaphore = asyncio.Semaphore(max_task) - tasks = [ - asyncio.create_task( - run_with_semaphore( - semaphore, - run_tidy, - args, - f, - clang_tidy_binary, - export_fixes_dir, - build_path, - profile_dir, - ) - ) - for f in files - ] - - try: - for i, coro in enumerate(asyncio.as_completed(tasks)): - result = await coro - if result.returncode != 0: - returncode = 1 - if result.returncode < 0: - result.stderr += f"{result.filename}: terminated by signal {-result.returncode}\n" - progress = f"[{i + 1: >{len(f'{len(files)}')}}/{len(files)}]" - runtime = f"[{result.elapsed:.1f}s]" - if not args.hide_progress: - print(f"{progress}{runtime} {' '.join(result.invocation)}") - if result.stdout: - print(result.stdout, end=("" if result.stderr else "\n")) - if result.stderr: - print(result.stderr) - except asyncio.CancelledError: - if not args.hide_progress: - print("\nCtrl-C detected, goodbye.") - for task in tasks: - task.cancel() - if delete_fixes_dir: - assert export_fixes_dir - shutil.rmtree(export_fixes_dir) - if profile_dir: - shutil.rmtree(profile_dir) - return - - if args.enable_check_profile and profile_dir: - # Ensure all clang-tidy stdout is flushed before printing profiling - sys.stdout.flush() - aggregated_data = aggregate_profiles(profile_dir) - if aggregated_data: - print_profile_data(aggregated_data) - else: - print("No profiling data found.") - - if combine_fixes: - if not args.hide_progress: - print(f"Writing fixes to {args.export_fixes} ...") - try: - assert export_fixes_dir - merge_replacement_files(export_fixes_dir, args.export_fixes) - except: - print("Error exporting fixes.\n", file=sys.stderr) - traceback.print_exc() - returncode = 1 - - if args.fix: - if not args.hide_progress: - print("Applying fixes ...") - try: - assert export_fixes_dir - apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir) - except: - print("Error applying fixes.\n", file=sys.stderr) - traceback.print_exc() - returncode = 1 - - if delete_fixes_dir: - assert export_fixes_dir - shutil.rmtree(export_fixes_dir) - if profile_dir: - shutil.rmtree(profile_dir) - sys.exit(returncode) - - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - pass diff --git a/src/ui/MainFrame.cpp b/src/ui/MainFrame.cpp index 09dc2a0..e69ecd9 100644 --- a/src/ui/MainFrame.cpp +++ b/src/ui/MainFrame.cpp @@ -142,8 +142,8 @@ bool MainFrame::Save() { if (m_doc.IsLoad()) { m_doc.Save(); return true; - } else - return false; + } + return false; } bool MainFrame::Close() {