Skip to content

Commit

Permalink
Make some changes to Natlink's message window (dictation-toolbox#214)
Browse files Browse the repository at this point in the history
* Fix Natlink message window pop-up menu initialization

Re: dictation-toolbox#184.

The message window's pop-up menu is now loaded from the executable
resource data.  It is easier to change now.

* Add an Exit item to the Natlink message window's File sub-menu

Re: dictation-toolbox#213.

This item is grayed out (disabled) by default, for now.  I've added
a separator between it and the File>Reload item.

* Adjust File>Exit menu item changes to do things the C++/Windows way

Re: dictation-toolbox#184.

That is, use boolean flags and a dedicated user window message to
convey updates to the output window.  This makes it easier to update
the pop-up menu later down the line, if desired, by adding and hand-
ling new flags.

* Add a new function for controlling Natlink's message window

Re: dictation-toolbox#213.

The new function is setMessageWindow().  I have documented it in the
NatlinkSource/natlink.txt file.

It is now necessary to call this function with a Python callback to
enable the message window.  The default callback will soon reside in
the natlinkcore code and be set from appsupp.cpp.

Since it is related, this changeset includes modifications to the
message window's File>Reload logic, re: dictation-toolbox#28.  The default callback
will do a narrower reloading of user modules.

* Fix global callbacks that fail without thread safety

This changes the CDragonCode::makeCallback() method to hold Python's
GIL regardless of whether Natlink thread safety is enabled.  A crash
occurs if this is not done.

* Adjust and fix the message window changes a little

Re: dictation-toolbox#213.

* Update appsupp

- Show the message window before natConnect().
- Remove the now unused reloadPython() method.

* Fix some indentation issues in two files

* Fix a problem with setMessageWindow()

* Stop releasing natlink objects on IDD_RELOAD

This can cause inconsistent state in user/library code.

---------
  • Loading branch information
drmfinlay authored Nov 26, 2024
1 parent a6b1b90 commit ce2784a
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 98 deletions.
30 changes: 12 additions & 18 deletions NatlinkSource/COM/appsupp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ std::string DoPyConfig(void) {
STDMETHODIMP CDgnAppSupport::Register( IServiceProvider * pIDgnSite )
{
OutputDebugString(TEXT("CDgnAppSupport::Register"));

// TODO check here with C++ if Natlink is enabled for the current
// windows user. If not, return S_OK early

// load and initialize the Python system
std::string init_error = DoPyConfig();
Py_Initialize();
Expand All @@ -287,17 +291,21 @@ STDMETHODIMP CDgnAppSupport::Register( IServiceProvider * pIDgnSite )
m_pDragCode = initModule();
m_pDragCode->setAppClass( this );

// start the message window, without a callback and with all menu items
// disabled, in order to use displayText()
// this is fine, the callback and menu will be set up later
m_pDragCode->setMessageWindow( NULL, 0x4 );

// simulate calling natlink.natConnect() except share the site object
BOOL bSuccess = m_pDragCode->natConnect( pIDgnSite );
if( !bSuccess ) {
OutputDebugString(
TEXT( "NatLink: failed to initialize NatSpeak interfaces") );
m_pDragCode->displayText( "Failed to initialize NatSpeak interfaces\r\n", TRUE ); // TODO: bug? won't show
m_pDragCode->displayText( "Failed to initialize NatSpeak interfaces\r\n", TRUE );
return S_OK;
}

// only now do we have the window to show info and possible error messages from before
// Python init

// show info and error messages
DisplayVersions(m_pDragCode);
if ( !init_error.empty()) {
OutputDebugStringA(init_error.c_str() );
Expand Down Expand Up @@ -418,17 +426,3 @@ STDMETHODIMP CDgnAppSupport::EndProcess( DWORD dwProcessID )
{
return S_OK;
}

//---------------------------------------------------------------------------
// This utility function reloads the Python interpreter. It is called from
// the display window menu and is useful for debugging during development of
// natlinkmain and natlinkutils. In normal use, we do not need to reload the
// Python interpreter.

void CDgnAppSupport::reloadPython()
{
// finalize the Python interpreter
OutputDebugString( TEXT( "CDgnAppSupport::reloadPython" ) );

PyImport_ReloadModule(m_pNatlinkModule);
}
3 changes: 0 additions & 3 deletions NatlinkSource/COM/appsupp.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ class ATL_NO_VTABLE CDgnAppSupport :
CDgnAppSupport();
~CDgnAppSupport();

// call this function to re-initialize the Python interface
void reloadPython();

DECLARE_REGISTRY_RESOURCEID(IDR_APPSUPP)
DECLARE_NOT_AGGREGATABLE(CDgnAppSupport)

Expand Down
137 changes: 96 additions & 41 deletions NatlinkSource/DragonCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,9 @@ void CDragonCode::removeDictObj(CDictationObject * pDictObj )

void CDragonCode::makeCallback( PyObject *pFunc, PyObject *pArgs )
{
// This procedure requires Python's GIL to be held.
PyGILState_STATE gstate = PyGILState_Ensure();

m_nCallbackDepth += 1;
PyObject * pRetn = PyEval_CallObject( pFunc, pArgs );
m_nCallbackDepth -= 1;
Expand Down Expand Up @@ -1032,6 +1035,8 @@ void CDragonCode::makeCallback( PyObject *pFunc, PyObject *pArgs )
onAttribChanged( DGNSRAC_MICSTATE );
}
}

PyGILState_Release( gstate );
}

//---------------------------------------------------------------------------
Expand Down Expand Up @@ -1203,36 +1208,56 @@ void CDragonCode::onTimer()

void CDragonCode::onMenuCommand( WPARAM wParam )
{
if( LOWORD(wParam) == IDD_RELOAD )
WORD nMenuItem = LOWORD( wParam );
switch ( nMenuItem )
{

// currently we do not support this operation if we are using thread
// support because I have not worked through the issues about what
// to do about the thread state
if( m_pThreadState )
{
return;
}
case IDD_RELOAD:
// note by DF:
// PyWin32, a library we all depend on, does not support
// interpreter reinitialization. Please see the following
// page for more information:
// https://mail.python.org/pipermail/python-win32/2013-January/012671.html
//
// With this limitation in mind, I have adjusted Natlink's
// reload mechanism for narrower code-reload.

// reload the Python subsystem
displayText( "Reloading Python subsystem...\r\n", FALSE, FALSE );

// Although we do not really care about the Python reference count
// we do want to reset the callbacks so we do not make a call into
// an obselete intrepreter.
// reset the callbacks set from Python
setChangeCallback( Py_None );
setBeginCallback( Py_None );
setTimerCallback( Py_None );
setTrayIcon( "", "", Py_None );

// We call this because the reinitialization will not free up the
// python objects. Note that we do free the COM objects but we do
// not free the Python objects. This means that we will have a
// minor memory leak but no object leaks (which would prevent
// shutdown of NatSpeak).
releaseObjects();
// note: we do not release objects here any more because it
// may result in inconsistent state in user/library code
//releaseObjects();

break;

case IDD_EXIT:
break;

default:
return;
}

m_pAppClass->reloadPython();
// invoke the message window callback, if one is set
if( m_pMessageWindowCallback )
{
makeCallback(
m_pMessageWindowCallback,
Py_BuildValue( "(i)", nMenuItem ) );
}

// do any post-callback work
switch ( nMenuItem )
{
case IDD_EXIT:
// kill the message window
setMessageWindow( Py_None );
break;
}
}

Expand Down Expand Up @@ -1536,16 +1561,10 @@ BOOL CDragonCode::initSecondWindow()
return FALSE;
}

// we store out class pointer in the window's extra data field so we
// we store our class pointer in the window's extra data field so we
// get called back when a menu message occurs
SetWindowLong( m_hMsgWnd, 0, (LONG)this );

// tell the thread about out message window
if( m_pSecdThrd )
{
m_pSecdThrd->setMsgWnd( m_hMsgWnd );
}

return TRUE;
}

Expand Down Expand Up @@ -1687,15 +1706,6 @@ BOOL CDragonCode::natConnect( IServiceProvider * pIDgnSite, BOOL bUseThreads )
}
#endif

// here we start the second thread for displaying messages; we only need
// this when we are called as a compatibility module

if( pIDgnSite != NULL )
{
OutputDebugString(L"CDragonCode::natConnect new MessageWindow");
m_pSecdThrd = new MessageWindow();
}

// Connect to NatSpeak

if( !initGetSiteObject( pIDgnSite ) )
Expand Down Expand Up @@ -1885,12 +1895,8 @@ BOOL CDragonCode::natDisconnect()
m_pIDgnSRTraining = NULL;
m_pISRCentral = NULL;

// shutdown the second thread
if( m_pSecdThrd )
{
delete m_pSecdThrd;
m_pSecdThrd = NULL;
}
// kill the message window
setMessageWindow( Py_None );

// destroy our hidden window
if( m_hMsgWnd )
Expand Down Expand Up @@ -3696,6 +3702,55 @@ BOOL CDragonCode::setTrayIcon(
return TRUE;
}

//---------------------------------------------------------------------------

BOOL CDragonCode::setMessageWindow(
PyObject * pCallback, DWORD dwFlags )
{
if( pCallback == Py_None )
{
Py_XDECREF( m_pMessageWindowCallback );
m_pMessageWindowCallback = NULL;

// shutdown the second thread
if( m_pSecdThrd )
{
delete m_pSecdThrd;
m_pSecdThrd = NULL;
}
}
else
{
// ignore this check if the menu is to be disabled
if( !(dwFlags & 0x04) )
{
NOTBEFORE_INIT( "setMessageWindow" );
}
Py_XINCREF( pCallback );
Py_XDECREF( m_pMessageWindowCallback );
m_pMessageWindowCallback = pCallback;

// start the second thread for displaying messages
if( !m_pSecdThrd )
{
m_pSecdThrd = new MessageWindow( dwFlags );
}

// otherwise, update the window
else
{
m_pSecdThrd->updateWindow( dwFlags );
}

// tell the second thread about our message window
m_pSecdThrd->setMsgWnd( m_hMsgWnd );
}

return TRUE;
}

//---------------------------------------------------------------------------

void CDragonCode::logCookie( const char * pText, QWORD qCookie )
{
DWORD *pCookie = (DWORD*)&qCookie;
Expand Down
5 changes: 5 additions & 0 deletions NatlinkSource/DragonCode.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class CDragonCode
m_pszLogFile = NULL;
m_bHasTrayIcon = FALSE;
m_pTrayIconCallback = NULL;
m_pMessageWindowCallback = NULL;
m_pMessageStack = NULL;
m_pIDgnSSvcOutputEventA=0;
m_pIDgnSSvcOutputEvent=0;
Expand Down Expand Up @@ -89,6 +90,7 @@ class CDragonCode
BOOL deleteWord( char * wordName );
BOOL setWordInfo( char * wordName, DWORD wordInfo );
BOOL setTrayIcon( char * iconName, char * toolTip, PyObject * pCallback );
BOOL setMessageWindow( PyObject * pCallback, DWORD dwflags = 0 );

PyObject * getCurrentModule();
PyObject * getCurrentUser();
Expand Down Expand Up @@ -267,6 +269,9 @@ class CDragonCode
BOOL m_bHasTrayIcon;
PyObject *m_pTrayIconCallback;

// set when the message window is started
PyObject * m_pMessageWindowCallback;

// This is what we call when we are ready to recume recognition
void doPausedProcessing( QWORD dwCookie );

Expand Down
Loading

0 comments on commit ce2784a

Please sign in to comment.