From b2060da716cd837410465aa2c86a1bfcb8d6f0d1 Mon Sep 17 00:00:00 2001 From: rdsza <17195815+rdsza@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:33:22 -0400 Subject: [PATCH 1/2] FEAT: Added Export Particles Button - Added Dialog boxex for Exporting Particle Picking Results by Job ID and if user created any custom groups. The filename to be saved gets auto poopulated to the Job ID or New Group name. Button is placed in Particle Picking Assets Panel. --- src/Makefile.am | 2 + src/gui/MyParticlePositionAssetPanel.cpp | 52 ++++- src/gui/MyParticlePositionAssetPanel.h | 1 + src/gui/MyPickingJobExportDialog.cpp | 222 ++++++++++++++++++++++ src/gui/MyPickingJobExportDialog.h | 42 ++++ src/gui/ProjectX_gui_assets.cpp | 7 + src/gui/ProjectX_gui_assets.h | 2 + src/gui/wxformbuilder/ProjectX_assets.fbp | 74 ++++++++ 8 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 src/gui/MyPickingJobExportDialog.cpp create mode 100644 src/gui/MyPickingJobExportDialog.h diff --git a/src/Makefile.am b/src/Makefile.am index d87f5d469..f131e1ec4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -118,6 +118,7 @@ noinst_HEADERS = core/stopwatch.h \ gui/MySettingsPanel.h \ gui/PickingResultsPanel.h \ gui/MyParticlePositionExportDialog.h \ + gui/MyPickingJobExportDialog.h \ gui/MyFrealignExportDialog.h \ gui/MyRelionExportDialog.h \ gui/MyRefinementPackageAssetPanel.h \ @@ -722,6 +723,7 @@ cisTEM_SOURCES = programs/projectx/projectx.cpp \ gui/ActionsPanelTm.cpp \ gui/MySettingsPanel.cpp \ gui/MyParticlePositionExportDialog.cpp \ + gui/MyPickingJobExportDialog.cpp \ gui/MyFrealignExportDialog.cpp \ gui/MyRelionExportDialog.cpp \ gui/MyRefinementPackageAssetPanel.cpp \ diff --git a/src/gui/MyParticlePositionAssetPanel.cpp b/src/gui/MyParticlePositionAssetPanel.cpp index 492a74ede..bbff910a2 100644 --- a/src/gui/MyParticlePositionAssetPanel.cpp +++ b/src/gui/MyParticlePositionAssetPanel.cpp @@ -1,5 +1,6 @@ //#include "../core/core_headers.h" #include "../core/gui_core_headers.h" +#include "MyPickingJobExportDialog.h" extern MyImageAssetPanel* image_asset_panel; @@ -8,6 +9,7 @@ MyParticlePositionAssetPanel::MyParticlePositionAssetPanel(wxWindow* parent) RenameAssetButton->Show(false); DisplayButton->Show(false); DisplayButton->Enable(false); + ExportButton->Show(true); Layout( ); Label0Title->SetLabel(""); @@ -354,7 +356,7 @@ void MyParticlePositionAssetPanel::NewFromParentClick(wxCommandEvent& event) { } void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { - // Get a text file which should have asset_id x_pos y_pos + // Get a text file which should have image_asset_id x_pos y_pos wxFileDialog openFileDialog(this, _("Open TXT file"), "", "", "TXT files (*.txt)|*.txt;*.txt", wxFD_OPEN | wxFD_FILE_MUST_EXIST); @@ -369,6 +371,18 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { MyErrorDialog* my_error = new MyErrorDialog(this); long image_asset_id; + // Create a new group named after the imported file (without extension) + wxString new_group_name = openFileDialog.GetFilename( ).BeforeLast('.'); + if ( new_group_name.IsEmpty( ) ) + new_group_name = openFileDialog.GetFilename( ); + current_group_number++; + all_groups_list->AddGroup(new_group_name); + all_groups_list->groups[all_groups_list->number_of_groups - 1].id = current_group_number; + AddGroupToDatabase(current_group_number, new_group_name.ToUTF8( ).data( ), current_group_number); + long new_group_index = all_groups_list->number_of_groups - 1; + + wxArrayLong imported_asset_indices; + ParticlePositionAsset temp_asset; temp_asset.pick_job_id = -1; temp_asset.picking_id = -1; @@ -387,7 +401,6 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { if ( current_line.IsEmpty( ) == false && current_line.StartsWith("#") == false ) { current_tokenizer.SetString(current_line); - wxPrintf("Current Line = %s, number_tokens = %li\n", current_line, current_tokenizer.CountTokens( )); if ( current_tokenizer.CountTokens( ) < 3 ) { my_error->ErrorText->AppendText(wxString::Format(wxT("Line %li contains less than 3 (%li) values and will be ignored\n"), counter, current_tokenizer.CountTokens( ))); @@ -440,6 +453,11 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { AddAsset(&temp_asset); //main_frame->current_project.database.AddNextParticlePositionAsset(temp_asset.asset_id, temp_asset.parent_id, temp_asset.pick_job_id, temp_asset.x_position, temp_asset.y_position); main_frame->current_project.database.AddNextParticlePositionAsset(&temp_asset); + + // Track this asset for the new group + long new_asset_index = all_assets_list->number_of_assets - 1; + all_groups_list->groups[new_group_index].AddMember(new_asset_index); + imported_asset_indices.Add(new_asset_index); } } } @@ -452,9 +470,20 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { main_frame->current_project.database.EndParticlePositionAssetInsert( ); + // Populate the new group in the database + if ( imported_asset_indices.GetCount( ) > 0 ) { + InsertArrayofGroupMembersToDatabase(new_group_index, &imported_asset_indices, NULL); + } + else { + // Nothing was imported; remove the empty group + all_groups_list->RemoveGroup(new_group_index); + RemoveGroupFromDatabase(current_group_number); + current_group_number--; + } + my_dialog->Destroy( ); - // errros? + // errors? if ( have_errors == true ) { my_error->ShowModal( ); @@ -462,6 +491,23 @@ void MyParticlePositionAssetPanel::ImportAssetClick(wxCommandEvent& event) { my_error->Destroy( ); + FillGroupList( ); + FillContentsList( ); + DirtyGroups( ); + main_frame->RecalculateAssetBrowser( ); + is_dirty = true; } } + +void MyParticlePositionAssetPanel::OnExportClick(wxCommandEvent& event) { + if ( ReturnNumberOfAssets( ) == 0 ) { + wxMessageBox(wxT("No particle positions to export."), wxT("Export"), + wxOK | wxICON_INFORMATION, this); + return; + } + + MyPickingJobExportDialog* dialog = new MyPickingJobExportDialog(this); + dialog->ShowModal( ); + dialog->Destroy( ); +} diff --git a/src/gui/MyParticlePositionAssetPanel.h b/src/gui/MyParticlePositionAssetPanel.h index 6da05309c..be17f8796 100644 --- a/src/gui/MyParticlePositionAssetPanel.h +++ b/src/gui/MyParticlePositionAssetPanel.h @@ -9,6 +9,7 @@ class MyParticlePositionAssetPanel : public MyAssetPanelParent { void ImportAssetClick(wxCommandEvent& event); void NewFromParentClick(wxCommandEvent& event); + void OnExportClick(wxCommandEvent& event); void EnableNewFromParentButton( ); diff --git a/src/gui/MyPickingJobExportDialog.cpp b/src/gui/MyPickingJobExportDialog.cpp new file mode 100644 index 000000000..4ca9d86f3 --- /dev/null +++ b/src/gui/MyPickingJobExportDialog.cpp @@ -0,0 +1,222 @@ +#include "../core/gui_core_headers.h" +#include "MyPickingJobExportDialog.h" + +extern MyMainFrame* main_frame; +extern MyImageAssetPanel* image_asset_panel; +extern MyParticlePositionAssetPanel* particle_position_asset_panel; + +// --------------------------------------------------------------------------- +// Construction +// --------------------------------------------------------------------------- + +MyPickingJobExportDialog::MyPickingJobExportDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, wxT("Export Particle Positions"), + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) { + + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + + main_sizer->Add(new wxStaticText(this, wxID_ANY, wxT("Select export source:")), + 0, wxALL, 10); + + JobComboBox = new wxComboBox(this, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxSize(360, -1), + 0, NULL, wxCB_READONLY); + main_sizer->Add(JobComboBox, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 10); + + ParticleCountText = new wxStaticText(this, wxID_ANY, wxEmptyString); + main_sizer->Add(ParticleCountText, 0, wxLEFT | wxBOTTOM, 10); + + main_sizer->Add(new wxStaticLine(this), 0, wxEXPAND | wxLEFT | wxRIGHT, 5); + + wxBoxSizer* button_sizer = new wxBoxSizer(wxHORIZONTAL); + wxButton* cancel_btn = new wxButton(this, wxID_ANY, wxT("Cancel")); + wxButton* export_btn = new wxButton(this, wxID_ANY, wxT("Export...")); + button_sizer->Add(cancel_btn, 0, wxALL, 5); + button_sizer->Add(export_btn, 0, wxALL, 5); + main_sizer->Add(button_sizer, 0, wxALIGN_RIGHT | wxALL, 5); + + SetSizer(main_sizer); + Fit( ); + Centre( ); + + FillComboBox( ); + + JobComboBox->Bind(wxEVT_COMBOBOX, &MyPickingJobExportDialog::OnJobSelectionChange, this); + export_btn->Bind(wxEVT_BUTTON, &MyPickingJobExportDialog::OnExportButtonClick, this); + cancel_btn->Bind(wxEVT_BUTTON, &MyPickingJobExportDialog::OnCancelButtonClick, this); +} + +// --------------------------------------------------------------------------- +// Populate combo box +// --------------------------------------------------------------------------- + +void MyPickingJobExportDialog::FillComboBox( ) { + entries_.clear( ); + long total = particle_position_asset_panel->ReturnNumberOfAssets( ); + + // --- Section 1: job-based entries --- + + // "All Jobs" + entries_.push_back({EntryType::AllJobs, 0, wxT("particles_all")}); + JobComboBox->Append(wxString::Format(wxT("All Jobs (%ld particles)"), total)); + + // Collect unique pick_job_ids in order of first appearance + std::vector seen_jobs; + for ( long i = 0; i < total; i++ ) { + int jid = particle_position_asset_panel->ReturnAssetPointer(i)->pick_job_id; + bool found = false; + for ( int v : seen_jobs ) + if ( v == jid ) { + found = true; + break; + } + if ( ! found ) + seen_jobs.push_back(jid); + } + + for ( int jid : seen_jobs ) { + long count = CountForEntry({(jid == -1 ? EntryType::ManualPicks : EntryType::Job), jid, wxEmptyString}); + if ( jid == -1 ) { + entries_.push_back({EntryType::ManualPicks, -1, wxT("particles_manual")}); + JobComboBox->Append(wxString::Format(wxT("Manually picked (%ld particles)"), count)); + } + else { + entries_.push_back({EntryType::Job, jid, + wxString::Format(wxT("particles_job_%d"), jid)}); + JobComboBox->Append(wxString::Format(wxT("Job #%d (%ld particles)"), jid, count)); + } + } + + // --- Section 2: user groups (groups 1+) --- + + int num_groups = particle_position_asset_panel->ReturnNumberOfGroups( ); + if ( num_groups > 1 ) { + // Dotted separator — stored as Separator entry, not selectable + entries_.push_back({EntryType::Separator, 0, wxEmptyString}); + JobComboBox->Append(wxT(" - - - - - - - - - - - - -")); + + for ( int g = 1; g < num_groups; g++ ) { + wxString gname = particle_position_asset_panel->ReturnGroupName(g); + long count = particle_position_asset_panel->ReturnGroupSize(g); + entries_.push_back({EntryType::Group, g, + wxT("particles_") + SanitizeFilename(gname)}); + JobComboBox->Append(wxString::Format(wxT("%s (%ld particles)"), gname, count)); + } + } + + JobComboBox->SetSelection(0); + last_valid_selection_ = 0; + ParticleCountText->SetLabel(wxString::Format(wxT("%ld particles will be exported"), total)); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +long MyPickingJobExportDialog::CountForEntry(const ComboEntry& e) const { + long total = particle_position_asset_panel->ReturnNumberOfAssets( ); + if ( e.type == EntryType::AllJobs ) + return total; + if ( e.type == EntryType::Group ) + return particle_position_asset_panel->ReturnGroupSize(e.value); + // Job or ManualPicks: count by pick_job_id + long count = 0; + for ( long i = 0; i < total; i++ ) + if ( particle_position_asset_panel->ReturnAssetPointer(i)->pick_job_id == e.value ) + count++; + return count; +} + +wxString MyPickingJobExportDialog::SanitizeFilename(const wxString& name) const { + wxString result = name; + result.Replace(wxT(" "), wxT("_")); + result.Replace(wxT("/"), wxT("_")); + result.Replace(wxT("\\"), wxT("_")); + return result; +} + +// --------------------------------------------------------------------------- +// Events +// --------------------------------------------------------------------------- + +void MyPickingJobExportDialog::OnJobSelectionChange(wxCommandEvent& event) { + int sel = JobComboBox->GetSelection( ); + if ( sel == wxNOT_FOUND ) + return; + + // Separator: snap back to last valid selection, do nothing + if ( entries_[sel].type == EntryType::Separator ) { + JobComboBox->SetSelection(last_valid_selection_); + return; + } + + last_valid_selection_ = sel; + long count = CountForEntry(entries_[sel]); + ParticleCountText->SetLabel(wxString::Format(wxT("%ld particles will be exported"), count)); + Fit( ); +} + +void MyPickingJobExportDialog::OnExportButtonClick(wxCommandEvent& event) { + int sel = JobComboBox->GetSelection( ); + if ( sel == wxNOT_FOUND ) + return; + const ComboEntry& entry = entries_[sel]; + if ( entry.type == EntryType::Separator ) + return; + + wxFileDialog save_dialog(this, wxT("Export particle positions"), + wxEmptyString, entry.default_filename + wxT(".txt"), + wxT("Text files (*.txt)|*.txt|All files (*.*)|*.*"), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if ( save_dialog.ShowModal( ) == wxID_CANCEL ) + return; + + wxFile output_file; + if ( ! output_file.Open(save_dialog.GetPath( ), wxFile::write) ) { + wxMessageBox(wxT("Could not open file for writing."), wxT("Export Error"), + wxOK | wxICON_ERROR, this); + return; + } + + //output_file.Write(wxT("# Image# X Y\n")); + + if ( entry.type == EntryType::Group ) { + // Export members of a user-created group + int g = entry.value; + long gsize = particle_position_asset_panel->ReturnGroupSize(g); + for ( long m = 0; m < gsize; m++ ) { + long asset_idx = particle_position_asset_panel->ReturnGroupMember(g, m); + ParticlePositionAsset* asset = particle_position_asset_panel->ReturnAssetPointer(asset_idx); + int image_pos = image_asset_panel->ReturnArrayPositionFromAssetID(asset->parent_id); + if ( image_pos < 0 ) + continue; + ImageAsset* img = image_asset_panel->ReturnAssetPointer(image_pos); + float x_pix = float(asset->x_position) / img->pixel_size + 1.0f; + float y_pix = float(img->y_size) - float(asset->y_position) / img->pixel_size + 1.0f; + output_file.Write(wxString::Format(wxT("%d %.2f %.2f\n"), asset->parent_id, x_pix, y_pix)); + } + } + else { + // Export by pick_job_id (AllJobs, Job, or ManualPicks) + long total = particle_position_asset_panel->ReturnNumberOfAssets( ); + for ( long i = 0; i < total; i++ ) { + ParticlePositionAsset* asset = particle_position_asset_panel->ReturnAssetPointer(i); + if ( entry.type != EntryType::AllJobs && asset->pick_job_id != entry.value ) + continue; + int image_pos = image_asset_panel->ReturnArrayPositionFromAssetID(asset->parent_id); + if ( image_pos < 0 ) + continue; + ImageAsset* img = image_asset_panel->ReturnAssetPointer(image_pos); + float x_pix = float(asset->x_position) / img->pixel_size + 1.0f; + float y_pix = float(img->y_size) - float(asset->y_position) / img->pixel_size + 1.0f; + output_file.Write(wxString::Format(wxT("%d %.2f %.2f\n"), asset->parent_id, x_pix, y_pix)); + } + } + + output_file.Close( ); + Close( ); +} + +void MyPickingJobExportDialog::OnCancelButtonClick(wxCommandEvent& event) { + Close( ); +} diff --git a/src/gui/MyPickingJobExportDialog.h b/src/gui/MyPickingJobExportDialog.h new file mode 100644 index 000000000..406841fc4 --- /dev/null +++ b/src/gui/MyPickingJobExportDialog.h @@ -0,0 +1,42 @@ +#ifndef _SRC_GUI_MYPICKINGJOBEPORTDIALOG_H_ +#define _SRC_GUI_MYPICKINGJOBEPORTDIALOG_H_ + +#include +#include +#include +#include + +class MyPickingJobExportDialog : public wxDialog { + public: + MyPickingJobExportDialog(wxWindow* parent); + + ~MyPickingJobExportDialog( ) {} + + private: + enum class EntryType { AllJobs, + Job, + ManualPicks, + Separator, + Group }; + + struct ComboEntry { + EntryType type; + int value; // pick_job_id for Job, group index for Group + wxString default_filename; // pre-filled in the save dialog + }; + + wxComboBox* JobComboBox; + wxStaticText* ParticleCountText; + std::vector entries_; + int last_valid_selection_ = 0; + + void FillComboBox( ); + long CountForEntry(const ComboEntry& e) const; + wxString SanitizeFilename(const wxString& name) const; + + void OnJobSelectionChange(wxCommandEvent& event); + void OnExportButtonClick(wxCommandEvent& event); + void OnCancelButtonClick(wxCommandEvent& event); +}; + +#endif diff --git a/src/gui/ProjectX_gui_assets.cpp b/src/gui/ProjectX_gui_assets.cpp index 0a0a34601..19c85907c 100644 --- a/src/gui/ProjectX_gui_assets.cpp +++ b/src/gui/ProjectX_gui_assets.cpp @@ -1072,6 +1072,11 @@ AssetPanelParent::AssetPanelParent( wxWindow* parent, wxWindowID id, const wxPoi bSizer28->Add( ResampleButton, 0, wxALL|wxEXPAND, 5 ); + ExportButton = new wxButton( m_panel3, wxID_ANY, wxT("Export"), wxDefaultPosition, wxDefaultSize, 0 ); + ExportButton->Hide(); + + bSizer28->Add( ExportButton, 0, wxALL|wxEXPAND, 5 ); + bSizer25->Add( bSizer28, 0, wxEXPAND, 5 ); @@ -1260,6 +1265,7 @@ AssetPanelParent::AssetPanelParent( wxWindow* parent, wxWindowID id, const wxPoi AddSelectedAssetButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::AddSelectedAssetClick ), NULL, this ); DisplayButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnDisplayButtonClick ), NULL, this ); ResampleButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnResampleClick ), NULL, this ); + ExportButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnExportClick ), NULL, this ); } AssetPanelParent::~AssetPanelParent() @@ -1305,6 +1311,7 @@ AssetPanelParent::~AssetPanelParent() AddSelectedAssetButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::AddSelectedAssetClick ), NULL, this ); DisplayButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnDisplayButtonClick ), NULL, this ); ResampleButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnResampleClick ), NULL, this ); + ExportButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AssetPanelParent::OnExportClick ), NULL, this ); } diff --git a/src/gui/ProjectX_gui_assets.h b/src/gui/ProjectX_gui_assets.h index 79f76bdd7..ed9af62df 100644 --- a/src/gui/ProjectX_gui_assets.h +++ b/src/gui/ProjectX_gui_assets.h @@ -344,6 +344,7 @@ class AssetPanelParent : public wxPanel wxButton* AddSelectedAssetButton; wxButton* DisplayButton; wxButton* ResampleButton; + wxButton* ExportButton; wxStaticLine* m_staticline6; wxStaticText* Label0Title; wxStaticText* Label0Text; @@ -391,6 +392,7 @@ class AssetPanelParent : public wxPanel virtual void AddSelectedAssetClick( wxCommandEvent& event ) { event.Skip(); } virtual void OnDisplayButtonClick( wxCommandEvent& event ) { event.Skip(); } virtual void OnResampleClick( wxCommandEvent& event ) { event.Skip(); } + virtual void OnExportClick( wxCommandEvent& event ) { event.Skip(); } public: diff --git a/src/gui/wxformbuilder/ProjectX_assets.fbp b/src/gui/wxformbuilder/ProjectX_assets.fbp index df333387e..cad9f5095 100644 --- a/src/gui/wxformbuilder/ProjectX_assets.fbp +++ b/src/gui/wxformbuilder/ProjectX_assets.fbp @@ -8639,6 +8639,80 @@ OnResampleClick + + 5 + wxALL|wxEXPAND + 0 + + 1 + 1 + 1 + 1 + + + + + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 1 + + 1 + + + 0 + 1 + wxID_ANY + Export + + 0 + + 0 + + + 0 + + 1 + ExportButton + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + Export particle positions to a text file + + wxFILTER_NONE + wxDefaultValidator + + + + + OnExportClick + + From 47b6b72a0d20da21179dc5c428243ef08200ff93 Mon Sep 17 00:00:00 2001 From: twagner9 Date: Fri, 10 Apr 2026 15:28:55 -0400 Subject: [PATCH 2/2] wxformbuilder generate code Tooltip existing in the wxformbuilder did not make it into the auto-generated ProjectX_gui_assets.cpp. Just ran Generate in the formbuilder project to produce this. --- src/gui/ProjectX_gui_assets.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/ProjectX_gui_assets.cpp b/src/gui/ProjectX_gui_assets.cpp index 19c85907c..5b2acc26b 100644 --- a/src/gui/ProjectX_gui_assets.cpp +++ b/src/gui/ProjectX_gui_assets.cpp @@ -1074,6 +1074,7 @@ AssetPanelParent::AssetPanelParent( wxWindow* parent, wxWindowID id, const wxPoi ExportButton = new wxButton( m_panel3, wxID_ANY, wxT("Export"), wxDefaultPosition, wxDefaultSize, 0 ); ExportButton->Hide(); + ExportButton->SetToolTip( wxT("Export particle positions to a text file") ); bSizer28->Add( ExportButton, 0, wxALL|wxEXPAND, 5 );