diff --git a/CMakeLists.txt b/CMakeLists.txt index 07b007a..db637b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,14 @@ cmake_minimum_required(VERSION 3.12.1) project(NNInteractive) + +# Mirror extension metadata here so Slicer's Extension Wizard can open either +# the repository root or the slicer_plugin directory as the extension source. +set(EXTENSION_HOMEPAGE "https://github.com/coendevente/SlicerNNInteractive") +set(EXTENSION_CONTRIBUTORS "Coen de Vente (University of Amsterdam)") +set(EXTENSION_DESCRIPTION "Deep learning-based framework for interactive segmentation of 3D images. The extension is available under an Apache-2.0 license, but the weights that are being downloaded when running the SlicerNNInteractive server are available under a Creative Commons Attribution Non Commercial Share Alike 4.0 license, as described in the original nnInteractive respository.") +set(EXTENSION_ICONURL "https://raw.githubusercontent.com/coendevente/SlicerNNInteractive/main/slicer_plugin/SlicerNNInteractive/Resources/Icons/SlicerNNInteractive.png") +set(EXTENSION_SCREENSHOTURLS "https://raw.githubusercontent.com/coendevente/SlicerNNInteractive/main/img/segmentation_result.jpg https://raw.githubusercontent.com/coendevente/SlicerNNInteractive/main/img/plugin_first_sight.png") +set(EXTENSION_DEPENDS "NA") + add_subdirectory(slicer_plugin) diff --git a/slicer_plugin/CMakeLists.txt b/slicer_plugin/CMakeLists.txt index ac5e943..e080fc8 100644 --- a/slicer_plugin/CMakeLists.txt +++ b/slicer_plugin/CMakeLists.txt @@ -19,6 +19,7 @@ include(${Slicer_USE_FILE}) #----------------------------------------------------------------------------- # Extension modules add_subdirectory(SlicerNNInteractive) +add_subdirectory(nninteractive) ## NEXT_MODULE if(BUILD_TESTING) diff --git a/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui b/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui index 3fc919e..eee6743 100644 --- a/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui +++ b/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui @@ -326,6 +326,33 @@ + + + + + + Undo history steps: + + + + + + + Maximum number of plugin prompt states kept for Ctrl+Z / Ctrl+Y. + + + 1 + + + 100 + + + 2 + + + + + diff --git a/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py b/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py index ed57771..077104c 100644 --- a/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py +++ b/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py @@ -9,6 +9,7 @@ import numpy as np from pathlib import Path +from collections import deque import slicer import qt @@ -178,10 +179,16 @@ def setup(self): self.all_prompt_buttons = {} self.setup_prompts() + self.max_history_depth = 2 + self._is_replaying_history = False + self._active_request_count = 0 + self._history_state = None + self.init_ui_functionality() _ = self.get_current_segment_id() self.previous_states = {} + self.reset_prompt_history() def init_ui_functionality(self): """ @@ -194,7 +201,15 @@ def init_ui_functionality(self): self.ui.Server.text = savedServer self.server = savedServer.rstrip("/") + settings = qt.QSettings() + saved_history_depth = settings.value( + "SlicerNNInteractive/maxHistoryDepth", self.max_history_depth + ) + self.set_max_history_depth(saved_history_depth, update_widget=False) + self.ui.sbHistoryDepth.value = self.max_history_depth + self.ui.Server.editingFinished.connect(self.update_server) + self.ui.sbHistoryDepth.valueChanged.connect(self.on_history_depth_changed) self.ui.pbTestServer.clicked.connect(self.test_server_connection) # Set initial prompt type @@ -235,6 +250,8 @@ def setup_shortcuts(self): "r": self.clear_current_segment, "Shift+L": self.submit_lasso_if_present, "t": self.toggle_prompt_type, # Add 'T' shortcut to toggle between positive/negative + "Ctrl+Z": self.undo_prompt, + "Ctrl+Y": self.redo_prompt, } self.shortcut_items = {} @@ -568,6 +585,192 @@ def remove_all_but_last_prompt(self): for i in range(n): node.RemoveNthControlPoint(0) + def make_history_entry(self, prompt_type, positive_click, payload, segmentation_mask): + return { + "type": prompt_type, + "positive": bool(positive_click), + "payload": payload, + "mask": np.array(segmentation_mask, dtype=np.uint8, copy=True), + } + + def reset_prompt_history(self, baseline_mask=None): + if baseline_mask is None: + image_data = self.get_image_data() + if image_data is not None: + baseline_mask = np.zeros(image_data.shape, dtype=np.uint8) + else: + baseline_mask = np.zeros((0,), dtype=np.uint8) + else: + baseline_mask = np.array(baseline_mask, dtype=np.uint8, copy=True) + + self._history_state = { + "base_mask": baseline_mask, + "past": deque(), + "future": deque(), + } + + def history_can_undo(self): + return bool(self._history_state and self._history_state["past"]) + + def history_can_redo(self): + return bool(self._history_state and self._history_state["future"]) + + def record_history_entry(self, prompt_type, positive_click, payload, segmentation_mask): + if self._is_replaying_history: + return + + if self._history_state is None: + self.reset_prompt_history() + + self._history_state["future"].clear() + if len(self._history_state["past"]) >= self.max_history_depth: + dropped_entry = self._history_state["past"].popleft() + self._history_state["base_mask"] = np.array( + dropped_entry["mask"], dtype=np.uint8, copy=True + ) + + entry = self.make_history_entry( + prompt_type=prompt_type, + positive_click=positive_click, + payload=payload, + segmentation_mask=segmentation_mask, + ) + self._history_state["past"].append(entry) + + def set_history_base_mask(self, baseline_mask): + baseline_mask = np.array(baseline_mask, dtype=np.uint8, copy=True) + if self._history_state is None: + self.reset_prompt_history(baseline_mask=baseline_mask) + return + + self._history_state["base_mask"] = baseline_mask + self._history_state["past"].clear() + self._history_state["future"].clear() + + def set_max_history_depth(self, max_history_depth, update_widget=True): + max_history_depth = max(1, int(max_history_depth)) + self.max_history_depth = max_history_depth + + if update_widget and hasattr(self, "ui") and hasattr(self.ui, "sbHistoryDepth"): + blocker = qt.QSignalBlocker(self.ui.sbHistoryDepth) + self.ui.sbHistoryDepth.value = self.max_history_depth + del blocker + + if self._history_state is None: + return + + while len(self._history_state["past"]) > self.max_history_depth: + dropped_entry = self._history_state["past"].popleft() + self._history_state["base_mask"] = np.array( + dropped_entry["mask"], dtype=np.uint8, copy=True + ) + + while len(self._history_state["future"]) > self.max_history_depth: + self._history_state["future"].pop() + + def on_history_depth_changed(self, value): + self.set_max_history_depth(value, update_widget=False) + settings = qt.QSettings() + settings.setValue("SlicerNNInteractive/maxHistoryDepth", self.max_history_depth) + + def build_history_payload(self, **kwargs): + payload = {} + for key, value in kwargs.items(): + if isinstance(value, np.ndarray): + payload[key] = np.array(value, copy=True) + elif isinstance(value, list): + payload[key] = list(value) + else: + payload[key] = value + return payload + + def cancel_pending_lasso(self): + lasso_node = self.prompt_types["lasso"]["node"] + if lasso_node is None: + return False + + if lasso_node.GetNumberOfControlPoints() < 1: + return False + + self.on_lasso_cancel_clicked() + self.ui.pbInteractionLasso.setChecked(False) + return True + + def apply_history_state(self, target_entries): + if self._history_state is None: + return + + base_mask = np.array(self._history_state["base_mask"], dtype=np.uint8, copy=True) + self.show_segmentation(base_mask) + upload_result = self.upload_segment_to_server() + if upload_result is None: + raise RuntimeError("Failed to synchronize base segment during history replay.") + + if not target_entries: + return + + self._is_replaying_history = True + try: + for entry in target_entries: + self.replay_history_entry(entry) + finally: + self._is_replaying_history = False + + self.show_segmentation(target_entries[-1]["mask"]) + + def replay_history_entry(self, entry): + prompt_type = entry["type"] + payload = entry["payload"] + positive_click = entry["positive"] + + if prompt_type == "point": + self.point_prompt( + xyz=payload["xyz"], + positive_click=positive_click, + sync=False, + record_history=False, + ) + elif prompt_type == "bbox": + self.bbox_prompt( + outer_point_one=payload["outer_point_one"], + outer_point_two=payload["outer_point_two"], + positive_click=positive_click, + sync=False, + record_history=False, + ) + elif prompt_type in ["lasso", "scribble"]: + self.lasso_or_scribble_prompt( + mask=payload["mask"], + positive_click=positive_click, + tp=prompt_type, + sync=False, + record_history=False, + ) + elif prompt_type == "clear": + # "Clear" is a no-op during replay because show_segmentation is called on masks + pass + else: + raise ValueError(f"Unknown prompt type '{prompt_type}' in history replay.") + + def undo_prompt(self): + if self.cancel_pending_lasso(): + return + + if self._active_request_count > 0 or not self.history_can_undo(): + return + + entry = self._history_state["past"].pop() + self._history_state["future"].appendleft(entry) + self.apply_history_state(list(self._history_state["past"])) + + def redo_prompt(self): + if self._active_request_count > 0 or not self.history_can_redo(): + return + + entry = self._history_state["future"].popleft() + self._history_state["past"].append(entry) + self.apply_history_state(list(self._history_state["past"])) + def on_place_button_clicked(self, checked, prompt_name): self.setup_prompts(skip_if_exists=True) @@ -643,11 +846,32 @@ def on_point_placed(self, caller, event): if volume_node: self.point_prompt(xyz=xyz, positive_click=self.is_positive) - @ensure_synched - def point_prompt(self, xyz=None, positive_click=False): + def point_prompt(self, xyz=None, positive_click=False, sync=True, record_history=True): """ Uploads point prompt to the server. """ + if sync: + return self._point_prompt_synched( + xyz=xyz, + positive_click=positive_click, + record_history=record_history, + ) + + return self._point_prompt_impl( + xyz=xyz, + positive_click=positive_click, + record_history=record_history, + ) + + @ensure_synched + def _point_prompt_synched(self, xyz=None, positive_click=False, record_history=True): + return self._point_prompt_impl( + xyz=xyz, + positive_click=positive_click, + record_history=record_history, + ) + + def _point_prompt_impl(self, xyz=None, positive_click=False, record_history=True): url = f"{self.server}/add_point_interaction" seg_response = self.request_to_server( @@ -662,6 +886,15 @@ def point_prompt(self, xyz=None, positive_click=False): debug_print(f"{positive_click} point prompt triggered! {xyz}") self.show_segmentation(unpacked_segmentation) + if record_history: + self.record_history_entry( + prompt_type="point", + positive_click=positive_click, + payload=self.build_history_payload(xyz=xyz), + segmentation_mask=unpacked_segmentation, + ) + + return unpacked_segmentation # # -- Bounding Box @@ -709,11 +942,35 @@ def _next(): self.prev_caller = caller - @ensure_synched - def bbox_prompt(self, outer_point_one, outer_point_two, positive_click=False): + def bbox_prompt(self, outer_point_one, outer_point_two, positive_click=False, sync=True, record_history=True): """ Uploads BBox prompt to the server. """ + if sync: + return self._bbox_prompt_synched( + outer_point_one=outer_point_one, + outer_point_two=outer_point_two, + positive_click=positive_click, + record_history=record_history, + ) + + return self._bbox_prompt_impl( + outer_point_one=outer_point_one, + outer_point_two=outer_point_two, + positive_click=positive_click, + record_history=record_history, + ) + + @ensure_synched + def _bbox_prompt_synched(self, outer_point_one, outer_point_two, positive_click=False, record_history=True): + return self._bbox_prompt_impl( + outer_point_one=outer_point_one, + outer_point_two=outer_point_two, + positive_click=positive_click, + record_history=record_history, + ) + + def _bbox_prompt_impl(self, outer_point_one, outer_point_two, positive_click=False, record_history=True): url = f"{self.server}/add_bbox_interaction" seg_response = self.request_to_server( @@ -729,6 +986,18 @@ def bbox_prompt(self, outer_point_one, outer_point_two, positive_click=False): seg_response.content, decompress=False ) self.show_segmentation(unpacked_segmentation) + if record_history: + self.record_history_entry( + prompt_type="bbox", + positive_click=positive_click, + payload=self.build_history_payload( + outer_point_one=outer_point_one, + outer_point_two=outer_point_two, + ), + segmentation_mask=unpacked_segmentation, + ) + + return unpacked_segmentation # # -- Lasso @@ -832,11 +1101,35 @@ def on_scribble_clicked(self, checked=False): # # -- Lasso/scribble # - @ensure_synched - def lasso_or_scribble_prompt(self, mask, positive_click=False, tp="lasso"): + def lasso_or_scribble_prompt(self, mask, positive_click=False, tp="lasso", sync=True, record_history=True): """ Uploads lasso or scribble prompt to the server. """ + if sync: + return self._lasso_or_scribble_prompt_synched( + mask=mask, + positive_click=positive_click, + tp=tp, + record_history=record_history, + ) + + return self._lasso_or_scribble_prompt_impl( + mask=mask, + positive_click=positive_click, + tp=tp, + record_history=record_history, + ) + + @ensure_synched + def _lasso_or_scribble_prompt_synched(self, mask, positive_click=False, tp="lasso", record_history=True): + return self._lasso_or_scribble_prompt_impl( + mask=mask, + positive_click=positive_click, + tp=tp, + record_history=record_history, + ) + + def _lasso_or_scribble_prompt_impl(self, mask, positive_click=False, tp="lasso", record_history=True): if np.sum(mask) == 0: return @@ -869,6 +1162,14 @@ def lasso_or_scribble_prompt(self, mask, positive_click=False, tp="lasso"): seg_response.content, decompress=False ) self.show_segmentation(unpacked_segmentation) + if record_history: + self.record_history_entry( + prompt_type=tp, + positive_click=positive_click, + payload=self.build_history_payload(mask=np.array(mask, dtype=np.uint8, copy=True)), + segmentation_mask=unpacked_segmentation, + ) + return unpacked_segmentation else: debug_print( f"lasso_or_scribble_prompt upload failed with status code: {seg_response.status_code}" @@ -891,7 +1192,7 @@ def on_scribble_finished(self, caller, event): else: return - mask = slicer.util.arrayFromSegmentBinaryLabelmap( + mask = self.get_segment_mask_or_empty( self.scribble_segment_node, label_name, self.get_volume_node() ) @@ -952,6 +1253,7 @@ def make_new_segment(self): # Make sure the right node is selected self.ui.editor_widget.setSegmentationNode(segmentation_node) self.segment_editor_node.SetSelectedSegmentID(new_segment_id) + self.reset_prompt_history() return segmentation_node, new_segment_id @@ -968,9 +1270,15 @@ def clear_current_segment(self): if selected_segment_id: debug_print(f"Clearing segment: {selected_segment_id}") - self.show_segmentation( - np.zeros(self.get_image_data().shape, dtype=np.uint8) + # Record the clear action in history before resetting prompt history or clearing + empty_mask = self.get_empty_mask() + self.record_history_entry( + prompt_type="clear", + positive_click=True, + payload={}, + segmentation_mask=empty_mask ) + self.show_segmentation(empty_mask) self.setup_prompts() self.upload_segment_to_server() else: @@ -981,6 +1289,7 @@ def show_segmentation(self, segmentation_mask): Updates the currently selected segment with the given binary mask array. """ t0 = time.time() + segmentation_mask = np.array(segmentation_mask, dtype=np.uint8, copy=True) self.previous_states["segment_data"] = segmentation_mask segmentationNode, selectedSegmentID = ( @@ -1013,8 +1322,6 @@ def show_segmentation(self, segmentation_mask): # (see https://github.com/coendevente/SlicerNNInteractive/issues/38) segmentationNode.GetSegmentation().CollapseBinaryLabelmaps() - del segmentation_mask - debug_print(f"show_segmentation took {time.time() - t0}") def get_segmentation_node(self): @@ -1065,6 +1372,39 @@ def get_current_segment_id(self): """ return self.ui.editor_widget.mrmlSegmentEditorNode().GetSelectedSegmentID() + def get_empty_mask(self): + image_data = self.get_image_data() + if image_data is None: + return np.zeros((0,), dtype=np.uint8) + + return np.zeros(image_data.shape, dtype=np.uint8) + + def get_segment_mask_or_empty(self, segmentation_node, segment_id, volume_node=None): + if segmentation_node is None or not segment_id: + return self.get_empty_mask() + + segment = segmentation_node.GetSegmentation().GetSegment(segment_id) + if segment is None: + return self.get_empty_mask() + + if volume_node is None: + volume_node = self.get_volume_node() + + try: + mask = slicer.util.arrayFromSegmentBinaryLabelmap( + segmentation_node, segment_id, volume_node + ) + except Exception as exc: + debug_print( + f"Falling back to empty mask for segment '{segment_id}': {exc}" + ) + return self.get_empty_mask() + + if mask is None: + return self.get_empty_mask() + + return np.array(mask, dtype=np.uint8, copy=True) + def get_segment_data(self): """ Gets the labelmap array (binary) of the currently selected segment. @@ -1073,7 +1413,7 @@ def get_segment_data(self): self.get_selected_segmentation_node_and_segment_id() ) - mask = slicer.util.arrayFromSegmentBinaryLabelmap( + mask = self.get_segment_mask_or_empty( segmentation_node, selected_segment_id, self.get_volume_node() ) seg_data_bool = mask.astype(bool) @@ -1099,6 +1439,9 @@ def selected_segment_changed(self): debug_print(f"selected_segment_changed: {selected_segment_changed}") + if selected_segment_changed and not self._is_replaying_history: + self.set_history_base_mask(segment_data.astype(np.uint8)) + return selected_segment_changed ############################################################################### @@ -1174,50 +1517,54 @@ def request_to_server(self, *args, **kwargs): Wraps requests.post in a try/except and shows error in pop up windows if necessary. """ - with slicer.util.tryWithErrorDisplay(_("Segmentation failed."), waitCursor=True): - - error_message = None - try: - response = requests.post(*args, **kwargs) - debug_print('response:', response) - except requests.exceptions.MissingSchema as e: - response = None - if self.server == "": - raise RuntimeError("It seems you have not set the server URL yet. You can configure it in the 'Configuration' tab.") - else: - raise RuntimeError(f"Server URL '{self.server}' is unreachable. You can edit the URL in the 'Configuration' tab.") - except requests.exceptions.ConnectionError as e: - response = None - raise RuntimeError(f"Failed to connect to server '{self.server}'. Please make sure the server is running and check the server URL in the 'Configuration' tab.") - except requests.exceptions.InvalidSchema as e: - append_text_to_error_message = "" - if not args[0].startswith("http://"): - append_text_to_error_message = "\n\nHint: Perhaps your Server URL in the 'Configuration' tab should start with 'http://'. For example, if your server runs on localhost and port 1527, 'localhost:1527' would not work as a Server URL, while 'http://localhost:1527' would." - raise RuntimeError(f'{e}{append_text_to_error_message}') - - if response.status_code != 200: - status_code = response.status_code - response = None - raise RuntimeError(f"Something has gone wrong with your request (Status code {status_code}).") - - t0 = time.time() - # Try to parse JSON and check for a specific error. - content_type = response.headers.get("Content-Type", "") - if "application/json" in content_type: - resp_json = response.json() - if resp_json.get("status") == "error": - if "No image uploaded" in resp_json.get("message", ""): - debug_print("No image has been uploaded to the server. Please upload an image first.") - self.upload_image_to_server() - self.upload_segment_to_server() - return self.request_to_server(*args, **kwargs) + self._active_request_count += 1 + try: + with slicer.util.tryWithErrorDisplay(_("Segmentation failed."), waitCursor=True): + + error_message = None + try: + response = requests.post(*args, **kwargs) + debug_print('response:', response) + except requests.exceptions.MissingSchema as e: + response = None + if self.server == "": + raise RuntimeError("It seems you have not set the server URL yet. You can configure it in the 'Configuration' tab.") else: - response = None - raise RuntimeError(f"Server error: {resp_json.get('message', 'Unknown error')}") - - debug_print('1157 took', time.time() - t0) - - return response + raise RuntimeError(f"Server URL '{self.server}' is unreachable. You can edit the URL in the 'Configuration' tab.") + except requests.exceptions.ConnectionError as e: + response = None + raise RuntimeError(f"Failed to connect to server '{self.server}'. Please make sure the server is running and check the server URL in the 'Configuration' tab.") + except requests.exceptions.InvalidSchema as e: + append_text_to_error_message = "" + if not args[0].startswith("http://"): + append_text_to_error_message = "\n\nHint: Perhaps your Server URL in the 'Configuration' tab should start with 'http://'. For example, if your server runs on localhost and port 1527, 'localhost:1527' would not work as a Server URL, while 'http://localhost:1527' would." + raise RuntimeError(f'{e}{append_text_to_error_message}') + + if response.status_code != 200: + status_code = response.status_code + response = None + raise RuntimeError(f"Something has gone wrong with your request (Status code {status_code}).") + + t0 = time.time() + # Try to parse JSON and check for a specific error. + content_type = response.headers.get("Content-Type", "") + if "application/json" in content_type: + resp_json = response.json() + if resp_json.get("status") == "error": + if "No image uploaded" in resp_json.get("message", ""): + debug_print("No image has been uploaded to the server. Please upload an image first.") + self.upload_image_to_server() + self.upload_segment_to_server() + return self.request_to_server(*args, **kwargs) + else: + response = None + raise RuntimeError(f"Server error: {resp_json.get('message', 'Unknown error')}") + + debug_print('1157 took', time.time() - t0) + + return response + finally: + self._active_request_count -= 1 def upload_image_to_server(self): """ diff --git a/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py b/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py index fe50756..2b38723 100644 --- a/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py +++ b/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py @@ -184,6 +184,8 @@ def runTest(self): mask, prompt_name ) + + self._verify_bounded_undo_redo(widget) finally: self.tearDown() @@ -271,6 +273,52 @@ def clamp(pt): ) return labelmap.astype(np.uint8) + def _current_segment_mask(self, widget): + segmentation_node, segment_id = widget.get_selected_segmentation_node_and_segment_id() + labelmap = slicer.util.arrayFromSegmentBinaryLabelmap( + segmentation_node, segment_id, widget.get_volume_node() + ) + return labelmap.astype(np.uint8) + + def _verify_bounded_undo_redo(self, widget): + widget.clear_current_segment() + + prompt_sequence = [ + positive([141, 114, 85]), + positive([109, 114, 58]), + positive([177, 114, 38]), + ] + + masks = [] + for interaction in prompt_sequence: + masks.append( + self._trigger_point_prompt(widget, interaction["coords"], interaction["positive"]) + ) + + widget.undo_prompt() + slicer.app.processEvents() + self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[1])) + + widget.undo_prompt() + slicer.app.processEvents() + self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[0])) + + widget.undo_prompt() + slicer.app.processEvents() + self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[0])) + + widget.redo_prompt() + slicer.app.processEvents() + self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[1])) + + widget.redo_prompt() + slicer.app.processEvents() + self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[2])) + + widget.redo_prompt() + slicer.app.processEvents() + self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[2])) + def _trigger_scribble_prompt(self, widget, interaction): mask = self._build_scribble_mask(widget, interaction) self._save_scribble_mask(interaction["mask_name"], mask) diff --git a/slicer_plugin/nninteractive/CMakeLists.txt b/slicer_plugin/nninteractive/CMakeLists.txt new file mode 100644 index 0000000..1ded2b2 --- /dev/null +++ b/slicer_plugin/nninteractive/CMakeLists.txt @@ -0,0 +1,63 @@ + +#----------------------------------------------------------------------------- +set(MODULE_NAME nninteractive) + +string(TOUPPER ${MODULE_NAME} MODULE_NAME_UPPER) + +#----------------------------------------------------------------------------- +add_subdirectory(Logic) +add_subdirectory(Widgets) + +#----------------------------------------------------------------------------- +set(MODULE_EXPORT_DIRECTIVE "Q_SLICER_QTMODULES_${MODULE_NAME_UPPER}_EXPORT") + +# Current_{source,binary} and Slicer_{Libs,Base} already included +set(MODULE_INCLUDE_DIRECTORIES + ${CMAKE_CURRENT_SOURCE_DIR}/Logic + ${CMAKE_CURRENT_BINARY_DIR}/Logic + ${CMAKE_CURRENT_SOURCE_DIR}/Widgets + ${CMAKE_CURRENT_BINARY_DIR}/Widgets + ) + +set(MODULE_SRCS + qSlicer${MODULE_NAME}Module.cxx + qSlicer${MODULE_NAME}Module.h + qSlicer${MODULE_NAME}ModuleWidget.cxx + qSlicer${MODULE_NAME}ModuleWidget.h + ) + +set(MODULE_MOC_SRCS + qSlicer${MODULE_NAME}Module.h + qSlicer${MODULE_NAME}ModuleWidget.h + ) + +set(MODULE_UI_SRCS + Resources/UI/qSlicer${MODULE_NAME}ModuleWidget.ui + ) + +set(MODULE_TARGET_LIBRARIES + vtkSlicer${MODULE_NAME}ModuleLogic + qSlicer${MODULE_NAME}ModuleWidgets + ) + +set(MODULE_RESOURCES + Resources/qSlicer${MODULE_NAME}Module.qrc + ) + +#----------------------------------------------------------------------------- +slicerMacroBuildLoadableModule( + NAME ${MODULE_NAME} + EXPORT_DIRECTIVE ${MODULE_EXPORT_DIRECTIVE} + INCLUDE_DIRECTORIES ${MODULE_INCLUDE_DIRECTORIES} + SRCS ${MODULE_SRCS} + MOC_SRCS ${MODULE_MOC_SRCS} + UI_SRCS ${MODULE_UI_SRCS} + TARGET_LIBRARIES ${MODULE_TARGET_LIBRARIES} + RESOURCES ${MODULE_RESOURCES} + WITH_GENERIC_TESTS + ) + +#----------------------------------------------------------------------------- +if(BUILD_TESTING) + add_subdirectory(Testing) +endif() diff --git a/slicer_plugin/nninteractive/Logic/CMakeLists.txt b/slicer_plugin/nninteractive/Logic/CMakeLists.txt new file mode 100644 index 0000000..d6f0cee --- /dev/null +++ b/slicer_plugin/nninteractive/Logic/CMakeLists.txt @@ -0,0 +1,26 @@ +project(vtkSlicer${MODULE_NAME}ModuleLogic) + +set(KIT ${PROJECT_NAME}) + +set(${KIT}_EXPORT_DIRECTIVE "VTK_SLICER_${MODULE_NAME_UPPER}_MODULE_LOGIC_EXPORT") + +set(${KIT}_INCLUDE_DIRECTORIES + ) + +set(${KIT}_SRCS + vtkSlicer${MODULE_NAME}Logic.cxx + vtkSlicer${MODULE_NAME}Logic.h + ) + +set(${KIT}_TARGET_LIBRARIES + ${ITK_LIBRARIES} + ) + +#----------------------------------------------------------------------------- +SlicerMacroBuildModuleLogic( + NAME ${KIT} + EXPORT_DIRECTIVE ${${KIT}_EXPORT_DIRECTIVE} + INCLUDE_DIRECTORIES ${${KIT}_INCLUDE_DIRECTORIES} + SRCS ${${KIT}_SRCS} + TARGET_LIBRARIES ${${KIT}_TARGET_LIBRARIES} + ) diff --git a/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.cxx b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.cxx new file mode 100644 index 0000000..0dff685 --- /dev/null +++ b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.cxx @@ -0,0 +1,73 @@ +/*============================================================================== + + Program: 3D Slicer + + Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +==============================================================================*/ + +// nninteractive Logic includes +#include "vtkSlicernninteractiveLogic.h" + +// MRML includes +#include + +// VTK includes +#include +#include +#include + +// STD includes +#include + +//---------------------------------------------------------------------------- +vtkStandardNewMacro(vtkSlicernninteractiveLogic); + +//---------------------------------------------------------------------------- +vtkSlicernninteractiveLogic::vtkSlicernninteractiveLogic() {} + +//---------------------------------------------------------------------------- +vtkSlicernninteractiveLogic::~vtkSlicernninteractiveLogic() {} + +//---------------------------------------------------------------------------- +void vtkSlicernninteractiveLogic::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} + +//--------------------------------------------------------------------------- +void vtkSlicernninteractiveLogic::SetMRMLSceneInternal(vtkMRMLScene* newScene) +{ + vtkNew events; + events->InsertNextValue(vtkMRMLScene::NodeAddedEvent); + events->InsertNextValue(vtkMRMLScene::NodeRemovedEvent); + events->InsertNextValue(vtkMRMLScene::EndBatchProcessEvent); + this->SetAndObserveMRMLSceneEventsInternal(newScene, events.GetPointer()); +} + +//----------------------------------------------------------------------------- +void vtkSlicernninteractiveLogic::RegisterNodes() +{ + assert(this->GetMRMLScene() != 0); +} + +//--------------------------------------------------------------------------- +void vtkSlicernninteractiveLogic::UpdateFromMRMLScene() +{ + assert(this->GetMRMLScene() != 0); +} + +//--------------------------------------------------------------------------- +void vtkSlicernninteractiveLogic::OnMRMLSceneNodeAdded(vtkMRMLNode* vtkNotUsed(node)) {} + +//--------------------------------------------------------------------------- +void vtkSlicernninteractiveLogic::OnMRMLSceneNodeRemoved(vtkMRMLNode* vtkNotUsed(node)) {} diff --git a/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.h b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.h new file mode 100644 index 0000000..876a0e8 --- /dev/null +++ b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.h @@ -0,0 +1,59 @@ +/*============================================================================== + + Program: 3D Slicer + + Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +==============================================================================*/ + +// .NAME vtkSlicernninteractiveLogic - slicer logic class for volumes manipulation +// .SECTION Description +// This class manages the logic associated with reading, saving, +// and changing propertied of the volumes + +#ifndef __vtkSlicernninteractiveLogic_h +#define __vtkSlicernninteractiveLogic_h + +// Slicer includes +#include "vtkSlicerModuleLogic.h" + +// MRML includes + +// STD includes +#include + +#include "vtkSlicernninteractiveModuleLogicExport.h" + +class VTK_SLICER_NNINTERACTIVE_MODULE_LOGIC_EXPORT vtkSlicernninteractiveLogic : public vtkSlicerModuleLogic +{ +public: + static vtkSlicernninteractiveLogic* New(); + vtkTypeMacro(vtkSlicernninteractiveLogic, vtkSlicerModuleLogic); + void PrintSelf(ostream& os, vtkIndent indent) override; + +protected: + vtkSlicernninteractiveLogic(); + ~vtkSlicernninteractiveLogic() override; + + void SetMRMLSceneInternal(vtkMRMLScene* newScene) override; + /// Register MRML Node classes to Scene. Gets called automatically when the MRMLScene is attached to this logic class. + void RegisterNodes() override; + void UpdateFromMRMLScene() override; + void OnMRMLSceneNodeAdded(vtkMRMLNode* node) override; + void OnMRMLSceneNodeRemoved(vtkMRMLNode* node) override; + +private: + vtkSlicernninteractiveLogic(const vtkSlicernninteractiveLogic&); // Not implemented + void operator=(const vtkSlicernninteractiveLogic&); // Not implemented +}; + +#endif diff --git a/slicer_plugin/nninteractive/Resources/Icons/nninteractive.png b/slicer_plugin/nninteractive/Resources/Icons/nninteractive.png new file mode 100644 index 0000000..9c5938e Binary files /dev/null and b/slicer_plugin/nninteractive/Resources/Icons/nninteractive.png differ diff --git a/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveFooBarWidget.ui b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveFooBarWidget.ui new file mode 100644 index 0000000..d2e6bed --- /dev/null +++ b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveFooBarWidget.ui @@ -0,0 +1,31 @@ + + + qSlicernninteractiveFooBarWidget + + + + 0 + 0 + 103 + 27 + + + + Foo bar + + + + 0 + + + + + Foo Bar + + + + + + + + diff --git a/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveModuleWidget.ui b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveModuleWidget.ui new file mode 100644 index 0000000..029b1fb --- /dev/null +++ b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveModuleWidget.ui @@ -0,0 +1,66 @@ + + + qSlicernninteractiveModuleWidget + + + + 0 + 0 + 525 + 319 + + + + nninteractive + + + + + + Display + + + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + qSlicerWidget + QWidget +
qSlicerWidget.h
+ 1 +
+ + ctkCollapsibleButton + QWidget +
ctkCollapsibleButton.h
+ 1 +
+ + qSlicernninteractiveFooBarWidget + QWidget +
qSlicernninteractiveFooBarWidget.h
+ 1 +
+
+ + +
diff --git a/slicer_plugin/nninteractive/Resources/qSlicernninteractiveModule.qrc b/slicer_plugin/nninteractive/Resources/qSlicernninteractiveModule.qrc new file mode 100644 index 0000000..2244c04 --- /dev/null +++ b/slicer_plugin/nninteractive/Resources/qSlicernninteractiveModule.qrc @@ -0,0 +1,5 @@ + + + Icons/nninteractive.png + + diff --git a/slicer_plugin/nninteractive/Testing/CMakeLists.txt b/slicer_plugin/nninteractive/Testing/CMakeLists.txt new file mode 100644 index 0000000..35f9732 --- /dev/null +++ b/slicer_plugin/nninteractive/Testing/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(Cxx) diff --git a/slicer_plugin/nninteractive/Testing/Cxx/CMakeLists.txt b/slicer_plugin/nninteractive/Testing/Cxx/CMakeLists.txt new file mode 100644 index 0000000..b7341cd --- /dev/null +++ b/slicer_plugin/nninteractive/Testing/Cxx/CMakeLists.txt @@ -0,0 +1,17 @@ +set(KIT qSlicer${MODULE_NAME}Module) + +#----------------------------------------------------------------------------- +set(KIT_TEST_SRCS + #qSlicer${MODULE_NAME}ModuleTest.cxx + ) + +#----------------------------------------------------------------------------- +slicerMacroConfigureModuleCxxTestDriver( + NAME ${KIT} + SOURCES ${KIT_TEST_SRCS} + WITH_VTK_DEBUG_LEAKS_CHECK + WITH_VTK_ERROR_OUTPUT_CHECK + ) + +#----------------------------------------------------------------------------- +#simple_test(qSlicer${MODULE_NAME}ModuleTest) diff --git a/slicer_plugin/nninteractive/Widgets/CMakeLists.txt b/slicer_plugin/nninteractive/Widgets/CMakeLists.txt new file mode 100644 index 0000000..7701b68 --- /dev/null +++ b/slicer_plugin/nninteractive/Widgets/CMakeLists.txt @@ -0,0 +1,42 @@ +project(qSlicer${MODULE_NAME}ModuleWidgets) + +set(KIT ${PROJECT_NAME}) + +set(${KIT}_EXPORT_DIRECTIVE "Q_SLICER_MODULE_${MODULE_NAME_UPPER}_WIDGETS_EXPORT") + +set(${KIT}_INCLUDE_DIRECTORIES + ) + +set(${KIT}_SRCS + qSlicer${MODULE_NAME}FooBarWidget.cxx + qSlicer${MODULE_NAME}FooBarWidget.h + ) + +set(${KIT}_MOC_SRCS + qSlicer${MODULE_NAME}FooBarWidget.h + ) + +set(${KIT}_UI_SRCS + ../Resources/UI/qSlicer${MODULE_NAME}FooBarWidget.ui + ) + +set(${KIT}_RESOURCES + ../Resources/qSlicer${MODULE_NAME}Module.qrc + ) + +set(${KIT}_TARGET_LIBRARIES + vtkSlicer${MODULE_NAME}ModuleLogic + ) + +#----------------------------------------------------------------------------- +SlicerMacroBuildModuleWidgets( + NAME ${KIT} + EXPORT_DIRECTIVE ${${KIT}_EXPORT_DIRECTIVE} + INCLUDE_DIRECTORIES ${${KIT}_INCLUDE_DIRECTORIES} + SRCS ${${KIT}_SRCS} + MOC_SRCS ${${KIT}_MOC_SRCS} + UI_SRCS ${${KIT}_UI_SRCS} + TARGET_LIBRARIES ${${KIT}_TARGET_LIBRARIES} + RESOURCES ${${KIT}_RESOURCES} + WRAP_PYTHONQT + ) diff --git a/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.cxx b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.cxx new file mode 100644 index 0000000..4bda2ec --- /dev/null +++ b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.cxx @@ -0,0 +1,63 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware Inc. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. + and was partially funded by NIH grant 3P41RR013218-12S1 + +==============================================================================*/ + +// FooBar Widgets includes +#include "qSlicernninteractiveFooBarWidget.h" +#include "ui_qSlicernninteractiveFooBarWidget.h" + +//----------------------------------------------------------------------------- +class qSlicernninteractiveFooBarWidgetPrivate : public Ui_qSlicernninteractiveFooBarWidget +{ + Q_DECLARE_PUBLIC(qSlicernninteractiveFooBarWidget); + +protected: + qSlicernninteractiveFooBarWidget* const q_ptr; + +public: + qSlicernninteractiveFooBarWidgetPrivate(qSlicernninteractiveFooBarWidget& object); + virtual void setupUi(qSlicernninteractiveFooBarWidget*); +}; + +// -------------------------------------------------------------------------- +qSlicernninteractiveFooBarWidgetPrivate::qSlicernninteractiveFooBarWidgetPrivate(qSlicernninteractiveFooBarWidget& object) + : q_ptr(&object) +{ +} + +// -------------------------------------------------------------------------- +void qSlicernninteractiveFooBarWidgetPrivate::setupUi(qSlicernninteractiveFooBarWidget* widget) +{ + this->Ui_qSlicernninteractiveFooBarWidget::setupUi(widget); +} + +//----------------------------------------------------------------------------- +// qSlicernninteractiveFooBarWidget methods + +//----------------------------------------------------------------------------- +qSlicernninteractiveFooBarWidget::qSlicernninteractiveFooBarWidget(QWidget* parentWidget) + : Superclass(parentWidget) + , d_ptr(new qSlicernninteractiveFooBarWidgetPrivate(*this)) +{ + Q_D(qSlicernninteractiveFooBarWidget); + d->setupUi(this); +} + +//----------------------------------------------------------------------------- +qSlicernninteractiveFooBarWidget ::~qSlicernninteractiveFooBarWidget() {} diff --git a/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.h b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.h new file mode 100644 index 0000000..ce4c1b4 --- /dev/null +++ b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.h @@ -0,0 +1,50 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware Inc. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. + and was partially funded by NIH grant 3P41RR013218-12S1 + +==============================================================================*/ + +#ifndef __qSlicernninteractiveFooBarWidget_h +#define __qSlicernninteractiveFooBarWidget_h + +// Qt includes +#include + +// FooBar Widgets includes +#include "qSlicernninteractiveModuleWidgetsExport.h" + +class qSlicernninteractiveFooBarWidgetPrivate; + +class Q_SLICER_MODULE_NNINTERACTIVE_WIDGETS_EXPORT qSlicernninteractiveFooBarWidget : public QWidget +{ + Q_OBJECT +public: + typedef QWidget Superclass; + qSlicernninteractiveFooBarWidget(QWidget* parent = 0); + ~qSlicernninteractiveFooBarWidget() override; + +protected slots: + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(qSlicernninteractiveFooBarWidget); + Q_DISABLE_COPY(qSlicernninteractiveFooBarWidget); +}; + +#endif diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModule.cxx b/slicer_plugin/nninteractive/qSlicernninteractiveModule.cxx new file mode 100644 index 0000000..86f74af --- /dev/null +++ b/slicer_plugin/nninteractive/qSlicernninteractiveModule.cxx @@ -0,0 +1,105 @@ +/*============================================================================== + + Program: 3D Slicer + + Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +==============================================================================*/ + +// nninteractive Logic includes +#include + +// nninteractive includes +#include "qSlicernninteractiveModule.h" +#include "qSlicernninteractiveModuleWidget.h" + +//----------------------------------------------------------------------------- +class qSlicernninteractiveModulePrivate +{ +public: + qSlicernninteractiveModulePrivate(); +}; + +//----------------------------------------------------------------------------- +// qSlicernninteractiveModulePrivate methods + +//----------------------------------------------------------------------------- +qSlicernninteractiveModulePrivate::qSlicernninteractiveModulePrivate() {} + +//----------------------------------------------------------------------------- +// qSlicernninteractiveModule methods + +//----------------------------------------------------------------------------- +qSlicernninteractiveModule::qSlicernninteractiveModule(QObject* _parent) + : Superclass(_parent) + , d_ptr(new qSlicernninteractiveModulePrivate) +{ +} + +//----------------------------------------------------------------------------- +qSlicernninteractiveModule::~qSlicernninteractiveModule() {} + +//----------------------------------------------------------------------------- +QString qSlicernninteractiveModule::helpText() const +{ + return "This is a loadable module that can be bundled in an extension"; +} + +//----------------------------------------------------------------------------- +QString qSlicernninteractiveModule::acknowledgementText() const +{ + return "This work was partially funded by NIH grant NXNNXXNNNNNN-NNXN"; +} + +//----------------------------------------------------------------------------- +QStringList qSlicernninteractiveModule::contributors() const +{ + QStringList moduleContributors; + moduleContributors << QString("John Doe (AnyWare Corp.)"); + return moduleContributors; +} + +//----------------------------------------------------------------------------- +QIcon qSlicernninteractiveModule::icon() const +{ + return QIcon(":/Icons/nninteractive.png"); +} + +//----------------------------------------------------------------------------- +QStringList qSlicernninteractiveModule::categories() const +{ + return QStringList() << "Examples"; +} + +//----------------------------------------------------------------------------- +QStringList qSlicernninteractiveModule::dependencies() const +{ + return QStringList(); +} + +//----------------------------------------------------------------------------- +void qSlicernninteractiveModule::setup() +{ + this->Superclass::setup(); +} + +//----------------------------------------------------------------------------- +qSlicerAbstractModuleRepresentation* qSlicernninteractiveModule::createWidgetRepresentation() +{ + return new qSlicernninteractiveModuleWidget; +} + +//----------------------------------------------------------------------------- +vtkMRMLAbstractLogic* qSlicernninteractiveModule::createLogic() +{ + return vtkSlicernninteractiveLogic::New(); +} diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModule.h b/slicer_plugin/nninteractive/qSlicernninteractiveModule.h new file mode 100644 index 0000000..0a172cc --- /dev/null +++ b/slicer_plugin/nninteractive/qSlicernninteractiveModule.h @@ -0,0 +1,68 @@ +/*============================================================================== + + Program: 3D Slicer + + Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +==============================================================================*/ + +#ifndef __qSlicernninteractiveModule_h +#define __qSlicernninteractiveModule_h + +// Slicer includes +#include "qSlicerLoadableModule.h" + +#include "qSlicernninteractiveModuleExport.h" + +class qSlicernninteractiveModulePrivate; + +class Q_SLICER_QTMODULES_NNINTERACTIVE_EXPORT qSlicernninteractiveModule : public qSlicerLoadableModule +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.slicer.modules.loadable.qSlicerLoadableModule/1.0"); + Q_INTERFACES(qSlicerLoadableModule); + +public: + typedef qSlicerLoadableModule Superclass; + explicit qSlicernninteractiveModule(QObject* parent = nullptr); + ~qSlicernninteractiveModule() override; + + qSlicerGetTitleMacro(tr("nninteractive")); + + QString helpText() const override; + QString acknowledgementText() const override; + QStringList contributors() const override; + + QIcon icon() const override; + + QStringList categories() const override; + QStringList dependencies() const override; + +protected: + /// Initialize the module. Register the volumes reader/writer + void setup() override; + + /// Create and return the widget representation associated to this module + qSlicerAbstractModuleRepresentation* createWidgetRepresentation() override; + + /// Create and return the logic associated to this module + vtkMRMLAbstractLogic* createLogic() override; + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(qSlicernninteractiveModule); + Q_DISABLE_COPY(qSlicernninteractiveModule); +}; + +#endif diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.cxx b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.cxx new file mode 100644 index 0000000..2a93d76 --- /dev/null +++ b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.cxx @@ -0,0 +1,57 @@ +/*============================================================================== + + Program: 3D Slicer + + Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +==============================================================================*/ + +// Qt includes +#include + +// Slicer includes +#include "qSlicernninteractiveModuleWidget.h" +#include "ui_qSlicernninteractiveModuleWidget.h" + +//----------------------------------------------------------------------------- +class qSlicernninteractiveModuleWidgetPrivate : public Ui_qSlicernninteractiveModuleWidget +{ +public: + qSlicernninteractiveModuleWidgetPrivate(); +}; + +//----------------------------------------------------------------------------- +// qSlicernninteractiveModuleWidgetPrivate methods + +//----------------------------------------------------------------------------- +qSlicernninteractiveModuleWidgetPrivate::qSlicernninteractiveModuleWidgetPrivate() {} + +//----------------------------------------------------------------------------- +// qSlicernninteractiveModuleWidget methods + +//----------------------------------------------------------------------------- +qSlicernninteractiveModuleWidget::qSlicernninteractiveModuleWidget(QWidget* _parent) + : Superclass(_parent) + , d_ptr(new qSlicernninteractiveModuleWidgetPrivate) +{ +} + +//----------------------------------------------------------------------------- +qSlicernninteractiveModuleWidget::~qSlicernninteractiveModuleWidget() {} + +//----------------------------------------------------------------------------- +void qSlicernninteractiveModuleWidget::setup() +{ + Q_D(qSlicernninteractiveModuleWidget); + d->setupUi(this); + this->Superclass::setup(); +} diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.h b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.h new file mode 100644 index 0000000..1d0aa50 --- /dev/null +++ b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.h @@ -0,0 +1,50 @@ +/*============================================================================== + + Program: 3D Slicer + + Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +==============================================================================*/ + +#ifndef __qSlicernninteractiveModuleWidget_h +#define __qSlicernninteractiveModuleWidget_h + +// Slicer includes +#include "qSlicerAbstractModuleWidget.h" + +#include "qSlicernninteractiveModuleExport.h" + +class qSlicernninteractiveModuleWidgetPrivate; +class vtkMRMLNode; + +class Q_SLICER_QTMODULES_NNINTERACTIVE_EXPORT qSlicernninteractiveModuleWidget : public qSlicerAbstractModuleWidget +{ + Q_OBJECT + +public: + typedef qSlicerAbstractModuleWidget Superclass; + qSlicernninteractiveModuleWidget(QWidget* parent = 0); + virtual ~qSlicernninteractiveModuleWidget(); + +public slots: + +protected: + QScopedPointer d_ptr; + + void setup() override; + +private: + Q_DECLARE_PRIVATE(qSlicernninteractiveModuleWidget); + Q_DISABLE_COPY(qSlicernninteractiveModuleWidget); +}; + +#endif