Skip to content

Commit 253b992

Browse files
committed
Added interactive viewer for slice and 3D views
Also made poll interval shorter to make views more responsive and enabled kernel debugger.
1 parent 95da038 commit 253b992

File tree

5 files changed

+205
-4
lines changed

5 files changed

+205
-4
lines changed

JupyterKernel/Resources/kernel-configure.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,155 @@ def _handle_upload(self, change=None, a=None, b=None):
393393
with open(self.path, 'wb') as f: f.write(content)
394394
print('Uploaded {0} ({1} bytes)'.format(self.filename, metadata['size']))
395395

396+
class InteractiveView(object):
397+
398+
def __init__(self, renderView):
399+
from ipyevents import Event
400+
from ipycanvas import Canvas
401+
402+
self.renderView = renderView
403+
404+
# Quality vs performance
405+
self.minTimBetweenRendersSec = 0.2
406+
self.compressionQuality = 50
407+
self.trackMouseMove = False # refresh if mouse is just moving (not dragging)
408+
409+
# If not receiving new rendering request for 10ms then a render is requested
410+
self.fullRenderRequestTimer = qt.QTimer()
411+
self.fullRenderRequestTimer.setSingleShot(True)
412+
self.fullRenderRequestTimer.setInterval(1000)
413+
self.fullRenderRequestTimer.connect('timeout()', self.fullRender)
414+
415+
# If not receiving new rendering request for 10ms then a render is requested
416+
self.quickRenderRequestTimer = qt.QTimer()
417+
self.quickRenderRequestTimer.setSingleShot(True)
418+
self.quickRenderRequestTimer.setInterval(200)
419+
self.quickRenderRequestTimer.connect('timeout()', self.quickRender)
420+
421+
# Get image size
422+
image = self.getImage()
423+
self.canvas = Canvas(width=image.width, height=image.height)
424+
self.canvasHeight=int(image.height)
425+
self.canvas.draw_image(image)
426+
427+
self.interactor = self.renderView.interactorStyle().GetInteractor()
428+
429+
self.dragging=False
430+
431+
self.interactionEvents = Event()
432+
self.interactionEvents.source = self.canvas
433+
self.interactionEvents.watched_events = [
434+
'dragstart', 'mouseenter', 'mouseleave',
435+
'mousedown', 'mouseup', 'mousemove',
436+
#'wheel', # commented out so that user can scroll through the notebook using mousewheel
437+
'keyup', 'keydown'
438+
]
439+
#self.interactionEvents.msg_throttle = 1 # does not seem to have effect
440+
self.interactionEvents.prevent_default_action = True
441+
self.interactionEvents.on_dom_event(self.handleInteractionEvent)
442+
443+
self.keyToSym = {
444+
'ArrowLeft': 'Left',
445+
'ArrowRight': 'Right',
446+
'ArrowUp': 'Up',
447+
'ArrowDown': 'Down',
448+
# TODO: more key codes could be added
449+
}
450+
451+
# Errors are not displayed when a widget is displayed,
452+
# this variable can be used to retrieve error messages
453+
self.error = None
454+
455+
# Enable logging of UI events
456+
self.logEvents = False
457+
self.loggedEvents = []
458+
459+
def getImage(self, compress=True, forceRender=True):
460+
from ipywidgets import Image
461+
slicer.app.processEvents()
462+
if forceRender:
463+
self.renderView.forceRender()
464+
screenshot = self.renderView.grab()
465+
bArray = qt.QByteArray()
466+
buffer = qt.QBuffer(bArray)
467+
buffer.open(qt.QIODevice.WriteOnly)
468+
if compress:
469+
screenshot.save(buffer, "JPG", self.compressionQuality)
470+
else:
471+
screenshot.save(buffer, "PNG")
472+
return Image(value=bArray.data(), width=screenshot.width(), height=screenshot.height())
473+
474+
def fullRender(self):
475+
self.fullRenderRequestTimer.stop()
476+
self.quickRenderRequestTimer.stop()
477+
self.canvas.draw_image(self.getImage(compress=False, forceRender=True))
478+
479+
def quickRender(self):
480+
self.fullRenderRequestTimer.stop()
481+
self.quickRenderRequestTimer.stop()
482+
self.canvas.draw_image(self.getImage(compress=True, forceRender=False))
483+
self.fullRenderRequestTimer.start()
484+
485+
def updateInteractorEventData(self, event):
486+
if event['event']=='keydown' or event['event']=='keyup':
487+
key = event['key']
488+
sym = self.keyToSym[key] if key in self.keyToSym.keys() else key
489+
self.interactor.SetKeySym(sym)
490+
if len(key) == 1:
491+
self.interactor.SetKeyCode(key)
492+
self.interactor.SetRepeatCount(1)
493+
else:
494+
self.interactor.SetEventPosition(event['offsetX'], self.canvasHeight-event['offsetY'])
495+
self.interactor.SetShiftKey(event['shiftKey'])
496+
self.interactor.SetControlKey(event['ctrlKey'])
497+
self.interactor.SetAltKey(event['altKey'])
498+
499+
def handleInteractionEvent(self, event):
500+
try:
501+
if self.logEvents:
502+
self.loggedEvents.append(event)
503+
renderNow=True
504+
if event['event']=='mousemove':
505+
if not self.dragging and not self.trackMouseMove:
506+
return
507+
renderNow = False
508+
self.updateInteractorEventData(event)
509+
self.interactor.MouseMoveEvent()
510+
self.canvas.draw_image(self.getImage(compress=True, forceRender=False))
511+
self.quickRenderRequestTimer.start()
512+
elif event['event']=='mousedown':
513+
self.dragging=True
514+
self.updateInteractorEventData(event)
515+
if event['button'] == 0:
516+
self.interactor.LeftButtonPressEvent()
517+
elif event['button'] == 2:
518+
self.interactor.RightButtonPressEvent()
519+
elif event['button'] == 1:
520+
self.interactor.MiddleButtonPressEvent()
521+
self.fullRender()
522+
elif event['event']=='mouseup':
523+
self.updateInteractorEventData(event)
524+
if event['button'] == 0:
525+
self.interactor.LeftButtonReleaseEvent()
526+
elif event['button'] == 2:
527+
self.interactor.RightButtonReleaseEvent()
528+
elif event['button'] == 1:
529+
self.interactor.MiddleButtonReleaseEvent()
530+
self.dragging=False
531+
self.fullRender()
532+
elif event['event']=='keydown':
533+
self.updateInteractorEventData(event)
534+
self.interactor.KeyPressEvent()
535+
self.interactor.CharEvent()
536+
self.fullRender()
537+
elif event['event']=='keyup':
538+
self.updateInteractorEventData(event)
539+
self.interactor.KeyReleaseEvent()
540+
self.fullRender()
541+
except Exception as e:
542+
self.error = str(e)
543+
544+
396545
def cliRunSync(module, node=None, parameters=None, delete_temporary_files=True, update_display=True):
397546

398547
node = slicer.cli.run(module, node=node, parameters=parameters, wait_for_completion=False,
@@ -417,6 +566,7 @@ def cliRunSync(module, node=None, parameters=None, delete_temporary_files=True,
417566
slicer.nb.displayViews = displayViews
418567
slicer.nb.displaySliceView = displaySliceView
419568
slicer.nb.display3DView = display3DView
569+
slicer.nb.InteractiveView = InteractiveView
420570

421571
slicer.nb.displayMarkups = displayMarkups
422572
slicer.nb.displayModel = displayModel

JupyterKernel/qSlicerJupyterKernelModule.cxx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
// XEUS includes
4545
#include "xeus/xkernel.hpp"
4646
#include "xeus/xkernel_configuration.hpp"
47+
#include "xeus-python/xdebugger.hpp"
4748

4849
#include "xSlicerInterpreter.h"
4950
#include "xSlicerServer.h"
@@ -273,7 +274,7 @@ void qSlicerJupyterKernelModule::startKernel(const QString& connectionFile)
273274
std::move(hist),
274275
nullptr,
275276
make_xSlicerServer,
276-
xeus::make_null_debugger);
277+
xpyt::make_python_debugger);
277278

278279
d->Kernel->start();
279280

@@ -467,3 +468,36 @@ void qSlicerJupyterKernelModule::setExecuteResultDataValue(const QString& str)
467468
Q_D(qSlicerJupyterKernelModule);
468469
d->ExecuteResultDataValue = str;
469470
}
471+
472+
//---------------------------------------------------------------------------
473+
double qSlicerJupyterKernelModule::pollIntervalSec()
474+
{
475+
Q_D(qSlicerJupyterKernelModule);
476+
if (d->Kernel == nullptr)
477+
{
478+
qCritical() << Q_FUNC_INFO << " failed: kernel has not started yet";
479+
return 0.0;
480+
}
481+
// TODO: uncomment when public API will be available
482+
// return reinterpret_cast<xSlicerServer*>(d->Kernel->p_server.get())->pollIntervalSec();
483+
qCritical() << Q_FUNC_INFO << " failed: not implemented";
484+
485+
return 0.0;
486+
}
487+
488+
//---------------------------------------------------------------------------
489+
void qSlicerJupyterKernelModule::setPollIntervalSec(double intervalSec)
490+
{
491+
Q_D(qSlicerJupyterKernelModule);
492+
if (d->Kernel == nullptr)
493+
{
494+
qCritical() << Q_FUNC_INFO << " failed: kernel has not started yet";
495+
return;
496+
}
497+
498+
// TODO: uncomment when public API will be available
499+
// reinterpret_cast<xSlicerServer*>(d->Kernel->p_server.get())->setPollIntervalSec(intervalSec);
500+
qCritical() << Q_FUNC_INFO << " failed: not implemented";
501+
}
502+
503+

JupyterKernel/qSlicerJupyterKernelModule.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ qSlicerJupyterKernelModule
3737
Q_INTERFACES(qSlicerLoadableModule);
3838
Q_PROPERTY(QString executeResultDataType READ executeResultDataType WRITE setExecuteResultDataType)
3939
Q_PROPERTY(QString executeResultDataValue READ executeResultDataValue WRITE setExecuteResultDataValue)
40+
Q_PROPERTY(double pollIntervalSec READ pollIntervalSec WRITE setPollIntervalSec)
4041

4142
public:
4243

@@ -67,11 +68,14 @@ qSlicerJupyterKernelModule
6768
void setExecuteResultDataType(const QString& str);
6869
QString executeResultDataValue();
6970
void setExecuteResultDataValue(const QString& str);
70-
71+
72+
double pollIntervalSec();
73+
7174
public slots:
7275

7376
void startKernel(const QString& connectionFile);
7477
void stopKernel();
78+
void setPollIntervalSec(double intervalSec);
7579

7680
protected:
7781

JupyterKernel/xSlicerServer.cxx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ xSlicerServer::xSlicerServer(zmq::context_t& context,
2323
const xeus::xconfiguration& c)
2424
: xserver_zmq(context, c)
2525
{
26-
// 50ms interval is sort enough so that users will not notice significant latency
26+
// 10ms interval is short enough so that users will not notice significant latency
2727
// yet it is long enough to minimize CPU load caused by polling.
28+
// 50ms causes too long delay in interactive widgets that handle mousemove events.
2829
m_pollTimer = new QTimer();
29-
m_pollTimer->setInterval(50);
30+
m_pollTimer->setInterval(10);
3031
QObject::connect(m_pollTimer, &QTimer::timeout, [=]() { poll(0); });
3132
}
3233

@@ -72,3 +73,12 @@ std::unique_ptr<xeus::xserver> make_xSlicerServer(zmq::context_t& context,
7273
return std::make_unique<xSlicerServer>(context, config);
7374
}
7475

76+
void xSlicerServer::setPollIntervalSec(double intervalSec)
77+
{
78+
m_pollTimer->setInterval(intervalSec*1000.0);
79+
}
80+
81+
double xSlicerServer::pollIntervalSec()
82+
{
83+
return m_pollTimer->interval() / 1000.0;
84+
}

JupyterKernel/xSlicerServer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class xSlicerServer : public xeus::xserver_zmq
2525

2626
virtual ~xSlicerServer();
2727

28+
void setPollIntervalSec(double intervalSec);
29+
double pollIntervalSec();
30+
2831
protected:
2932

3033
void start_impl(zmq::multipart_t& message) override;

0 commit comments

Comments
 (0)