Allow cancelling a scan session.

- perform scanning in a thread
 - change the label of the scan button
 - trigger cancel with the same button and with the ESC key
 - process all GUI updates in asynchronous mode.

Do not use a top window as parent of popups:
 - If a top window goes away in an application with multiple instances of
 XInsaneWidget, any call to a scanner widget leads to a crash.

Minor changes.
This commit is contained in:
Saleem Edah-Tally
2025-07-11 21:10:59 +02:00
parent 3350f86ddf
commit 4b23b1f3de
12 changed files with 206 additions and 215 deletions

View File

@@ -30,5 +30,4 @@ target_link_libraries(insanewidget minutils stampwidget
${wxWidgets_LIBRARIES}
${LIBINSANE_LIBRARIES}
${PODOFO_LIBRARIES}
${NETPBM_LIBRARIES}
${PAPER_LIBRARIES})

View File

@@ -22,7 +22,6 @@
using namespace std;
#define IERR(e) ("[" + to_string(e) + "] " + lis_strerror(e))
static bool gs_cancelRequested = false;
InsaneWorker::InsaneWorker ( InsaneWorkerEvent * evh )
{
@@ -306,6 +305,8 @@ bool InsaneWorker::Scan(const std::string& dir, const std::string& basename,
return false;
}
}
if (m_cancelRequested)
return false;
auto makeFileName = [&] (int index)
{
// std:format is not friendly with a variable padwidth; requires a literal format.
@@ -375,12 +376,13 @@ bool InsaneWorker::Scan(const std::string& dir, const std::string& basename,
m_evh->OnPageStartScan(filePath, pageIndex, imageAttributes);
do
{
if (gs_cancelRequested)
if (m_cancelRequested)
{
session->cancel(session);
m_rootSourceItem->close(m_rootSourceItem);
if (m_evh)
m_evh->OnSessionCancelled(filePath);
gs_cancelRequested = false;
m_cancelRequested = false;
return false;
}
try
@@ -410,7 +412,7 @@ bool InsaneWorker::Scan(const std::string& dir, const std::string& basename,
catch (std::bad_alloc& e)
{
m_rootSourceItem->close(m_rootSourceItem);
cout << "ABORT: " << e.what() << " - could not allocate " << bytesPerRow << " bytes." << endl;
cerr << "ABORT: " << e.what() << " - could not allocate " << bytesPerRow << " bytes." << endl;
if (m_evh)
m_evh->OnError("Insufficient system RAM.");
return false;
@@ -516,7 +518,7 @@ std::string InsaneWorker::ToLower(const std::string& input)
void InsaneWorker::Cancel()
{
gs_cancelRequested = true;
m_cancelRequested = true;
}
std::pair<int, int> InsaneWorker::UpdateStartAndIncrement(const int startPageIndex, const int increment,

View File

@@ -88,6 +88,7 @@ private:
lis_item * m_sourceItem = nullptr;
InsaneWorkerEvent * m_evh = nullptr;
bool m_cancelRequested = false;
std::vector<DeviceDescriptor> m_devices;
std::string m_deviceId;
std::string m_source = "FlatBed";

View File

@@ -40,6 +40,45 @@ private:
wxWeakRef<XInsaneWidget> m_owner = nullptr;
};
// ----------------------------------------------------------------------------
class BackgroundScan : public wxThread
{
public:
BackgroundScan(XInsaneWidget * insaneWidget, InsaneWorker * insaneWorker,
const wxString& dir, const wxString& basename,
int start = 0, int padWidth = 2, int increment = 1)
{
m_insaneWidget = insaneWidget;
m_insaneWorker = insaneWorker;
m_dir = dir;
m_basename = basename;
m_start = start;
m_padWidth = padWidth;
m_increment = increment;
}
ExitCode Entry() override
{
/*
* We refrain from using a mutex/critical section.
* If 2 instances of XInsaneWidget in a single application scan on the same
* device simultaneously, the first one that starts scanning prevails and
* the second one triggers a "Device busy" error.
* If 2 instances of XInsaneWidget in a single application scan on different
* devices simultaneously, both jobs succeed.
* Since there cannot be concurrency leading to a crash ('Device busy'), we
* do not block the next line. It may limit an application that needs to
* scan from multiple devices.
*/
m_insaneWorker->Scan(m_dir.ToStdString(), m_basename.ToStdString(), m_start, m_padWidth, m_increment);
return (ExitCode) 0;
}
private:
wxWeakRef<XInsaneWidget> m_insaneWidget = nullptr;
InsaneWorker * m_insaneWorker = nullptr;
wxString m_dir, m_basename;
int m_start = 0, m_padWidth = 2, m_increment = 1;
};
// ----------------------------------------------------------------------------
class ScanProjectHandler : public InsaneWorkerEvent
{
@@ -117,6 +156,41 @@ public:
{
m_stampDescriptors = descriptors;
}
// WIP means InsaneWorker is busy, not the position in a scan project.
void SetWip(bool wip)
{
m_wip = wip;
}
bool GetWip() const
{
return m_wip;
}
/* Enable/disable the label and destination file controls per scanning and
* idle mode.
* Do not include in Reset() because the status of the scan button is
* independent of the rest.
*/
void UpdateControlsStatus(bool enable)
{
wxTheApp->CallAfter([this, enable] ()
{
m_owner->lblNewDoc->Enable(enable);
m_owner->txtNewDoc->Enable(enable);
if (enable)
{
m_owner->btnScan->Enable(enable);
m_owner->btnScan->SetLabelText(_("Scan"));
}
else
{
/*Only XInsaneWidget::Setup() and XInsaneWidget::CancelScanning()
* disable the scan button.
*/
m_owner->btnScan->SetLabelText(_("Cancel"));
}
}
);
}
std::pair<int, int> GetStartAndIncrement(InsaneWorker * insaneWorker)
{
wxASSERT_MSG(insaneWorker != nullptr, "insaneWorker is NULL.");
@@ -130,26 +204,35 @@ public:
{
cerr << message << endl;
Reset();
MiscTools::MessageBox(_("A scan library error occurred."), true);
UpdateControlsStatus(true);
const wxString msg = _("A scan library error occurred.");
MiscTools::AsyncMessageBox(msg, true);
}
void OnError ( const std::string& message ) override
{
cerr << message << endl;
Reset();
MiscTools::MessageBox(_("A general error occurred."), true);
UpdateControlsStatus(true);
const wxString msg = _("A general error occurred.");
MiscTools::AsyncMessageBox(msg, true);
}
void OnSessionReadError(const std::string & filePath) override
{
const wxString msg = _("A session read error occurred.");
cerr << msg << endl;
Reset();
MiscTools::MessageBox(msg, true);
UpdateControlsStatus(true);
MiscTools::AsyncMessageBox(msg, true);
}
void OnSessionCancelled(const std::string & filePath) override
{
const wxString msg = _("Session cancelled.");
if (wxFileExists(filePath))
wxRemoveFile(filePath);
Reset();
MiscTools::MessageBox(msg, true);
UpdateControlsStatus(true);
const wxString msg = _("Session cancelled.");
MiscTools::AsyncMessageBox(msg, true);
m_sb->CallAfter(&TimeredStatusBar::SetTransientText, msg, 3000);
}
// Every time a page is fully scanned.
void OnPageEndScan(const std::string & filePath, uint pageIndex,
@@ -158,7 +241,7 @@ public:
m_startPageIndex = pageIndex;
m_pixelFiles[pageIndex] = {filePath, imageAttributes};
auto informProgress = [&] ()
auto informProgress = [this] ()
{
if (!m_sb || (m_total == 1))
return;
@@ -175,20 +258,19 @@ public:
wxString upperBoundMessage = _(". Turn the whole stack of pages.");
msg += upperBoundMessage;
}
m_sb->SetStatusText(msg);
m_sb->CallAfter(&wxStatusBar::SetStatusText, msg, 0);
};
if (m_outputType != PDF)
{
// Convert pixel file to PNG using netpbm.
// Convert pixel file to PNG using wxImage.
if (!PixelToImageWriter::Convert(filePath, imageAttributes.width, imageAttributes.height,
m_stampDescriptors, m_outputType))
{
const wxString msg = _("Failed to create output image.");
cerr << msg << endl;
if (m_sb)
m_sb->SetTransientText(msg);
m_sb->CallAfter(&TimeredStatusBar::SetTransientText, msg, 3000);
}
wxRemoveFile(filePath);
informProgress();
@@ -214,7 +296,7 @@ public:
const wxString msg = _("Failed to add page to PDF document.");
cerr << msg << endl;
if (m_sb)
m_sb->SetTransientText(msg);
m_sb->CallAfter(&TimeredStatusBar::SetTransientText, msg, 3000);
}
wxRemoveFile(filePath);
informProgress();
@@ -251,16 +333,19 @@ public:
m_pixelToPdfWriter.reset((new PixelToPdfWriter())); // For next scan project.
}
Reset();
m_owner->txtNewDoc->Clear();
m_owner->txtNewDoc->CallAfter(&wxTextCtrl::SetValue, wxString());
if (m_sb)
{
const wxString msg = _("Finished.");
m_sb->SetTransientText(msg);
m_sb->CallAfter(&TimeredStatusBar::SetTransientText, msg, 3000);
}
}
UpdateControlsStatus(true);
m_wip = false;
}
void OnStartScanSession(uint pageIndex, const ImageAttributes& imageAttributes) override
{
// Dealing with stamps only.
if (!m_stampDescriptors)
return;
for (StampDescriptor * descriptor : *m_stampDescriptors)
@@ -277,6 +362,7 @@ public:
m_increment = 1;
m_totalEven = 0;
m_pixelFiles.clear();
m_wip = false;
}
private:
@@ -295,6 +381,7 @@ private:
int m_totalEven = 0;
int m_startPageIndex = 0;
int m_increment = 1;
bool m_wip = false; // Is InsaneWorker busy?
};
@@ -316,7 +403,12 @@ void XInsaneWidget::Setup()
lblNewDoc->Bind ( wxEVT_RIGHT_UP, &XInsaneWidget::OnLblNewDocRightClick, this );
txtNewDoc->Bind ( wxEVT_KEY_UP, &XInsaneWidget::OnTxtNewDocKeyPressed, this );
m_ptwScannerWidget = std::unique_ptr<wxPopupTransientWindow> (new wxPopupTransientWindow ( wxApp::GetGUIInstance()->GetTopWindow() ));
/*
* Don't use GetTopWindow() as parent. If it goes away in an application with
* multiples instances of XInsaneWidget, any call to the scanner widget leads
* to a crash.
*/
m_ptwScannerWidget = std::make_unique<wxPopupTransientWindow> (GetParent());
m_ptwScannerWidget->Show ( false );
m_scanProject = std::make_unique<ScanProjectHandler>(this, m_sb);
m_insaneWorker = std::make_unique<InsaneWorker>(m_scanProject.get());
@@ -336,12 +428,17 @@ void XInsaneWidget::Setup()
// Show a popup to specifiy the number of pages to scan and back-sided scanning.
void XInsaneWidget::OnLblNewDocRightClick ( wxMouseEvent& evt )
{
if (!lblNewDoc->IsEnabled())
{
evt.Skip();
return;
}
/*
* The previous ConfigEditorPopup is deleted here, which commits the
* parameters to the config file. At any time, the current parameter values
* can be lost if a crash occurs.
*/
m_pageStack.reset(new ConfigEditorPopup(wxApp::GetGUIInstance()->GetTopWindow(), m_config));
m_pageStack.reset(new ConfigEditorPopup(GetParent(), m_config));
PopupTransientWindow * ptw = m_pageStack->CreatePopup();
if ( !ptw )
{
@@ -403,6 +500,12 @@ void XInsaneWidget::OnBtnScanRightClick ( wxMouseEvent& evt )
// Start scanning.
void XInsaneWidget::OnBtnScanClick ( wxMouseEvent& evt )
{
if (m_scanProject->GetWip())
{
CancelScanning();
evt.Skip();
return;
}
const wxString dest = txtNewDoc->GetValue();
if (dest.IsEmpty())
{
@@ -453,9 +556,14 @@ void XInsaneWidget::OnBtnScanClick ( wxMouseEvent& evt )
std::pair<int, int> startAndIncrement = m_scanProject->GetStartAndIncrement(m_insaneWorker.get());
const int padWidth = ( ushort ) m_config->Read (_T("/Scanner/Counter/Length"), 2 );
bool res = m_insaneWorker->Scan(destFile.GetPath().ToStdString(),
destFile.GetName().ToStdString(),
startAndIncrement.first, padWidth, startAndIncrement.second);
m_scanProject->UpdateControlsStatus(false);
BackgroundScan * bgScan = new BackgroundScan(this, m_insaneWorker.get(),
destFile.GetPath().ToStdString(),
destFile.GetName().ToStdString(),
startAndIncrement.first, padWidth,
startAndIncrement.second);
m_scanProject->SetWip(true);
bgScan->Run();
}
evt.Skip();
}
@@ -468,8 +576,11 @@ void XInsaneWidget::ResetScanProject()
void XInsaneWidget::CancelScanning()
{
if (m_insaneWorker)
if (m_insaneWorker && m_scanProject->GetWip())
{
m_insaneWorker->Cancel();
btnScan->Enable(false);
}
}
void XInsaneWidget::EnableScanButton(bool enable)

View File

@@ -63,7 +63,7 @@ public:
XInsaneWidget( wxWindow* parent, TimeredStatusBar * sb, wxConfig * config, wxWindowID id = SYMBOL_INSANEWIDGET_IDNAME, const wxPoint& pos = SYMBOL_INSANEWIDGET_POSITION, const wxSize& size = SYMBOL_INSANEWIDGET_SIZE, long style = SYMBOL_INSANEWIDGET_STYLE );
void ResetScanProject();
void CancelScanning(); // Not tested, probably doesn't work as intended.
void CancelScanning();
void EnableScanButton(bool enable); // For CallAfter.
void Setup();
private: