Beispiel #1
0
class FileProgressBar(QWidget):
    """Simple progress bar with a label"""
    MAX_LABEL_LENGTH = 60

    def __init__(self, parent, *args, **kwargs):
        QWidget.__init__(self, parent)
        self.pap = parent
        self.status_text = QLabel(self)
        self.bar = QProgressBar(self)
        self.bar.setRange(0, 0)
        layout = QVBoxLayout()
        layout.addWidget(self.status_text)
        layout.addWidget(self.bar)
        self.setLayout(layout)

    def __truncate(self, text):
        ellipsis = '...'
        part_len = (self.MAX_LABEL_LENGTH - len(ellipsis)) / 2.0
        left_text = text[:int(math.ceil(part_len))]
        right_text = text[-int(math.floor(part_len)):]
        return left_text + ellipsis + right_text

    @Slot(str, int)
    def set_label_file(self, file, size):
        text = self.__truncate(file)
        status_str = 'Downloading file list: {0} ({1})'.format(
            text, humanize.naturalsize(size))
        self.status_text.setText(status_str)

    def set_bounds(self, a, b):
        self.bar.setRange(a, b)

    def reset_files(self):
        self.status_text.setText("  Downloading file(s)...")
        self.bar.show()

    def reset_status(self):
        self.status_text.setText("  Download Complete!")
        self.bar.hide()

    @Slot(str, int, int, int)
    def update_progress(self, file, num_chunks, bytes_recv, total_bytes):
        text = "  Downloading {0} - {1}/{2} (Chunk {3})"
        self.status_text.setText(
            text.format(file, humanize.naturalsize(bytes_recv),
                        humanize.naturalsize(total_bytes), num_chunks))
        self.bar.setValue(bytes_recv)
        def __on_reporter_created(reporter):
            initial_geometry = self.geometry()

            self.progress_bar_container_widget.setVisible(True)
            self.progress_bar_divider_line.setVisible(True)

            progress_bar = QProgressBar(self.progress_bar_container_widget)
            progress_bar.setAlignment(Qt.AlignCenter)

            reporter.progress_bar = progress_bar
            self.__reporter_map[reporter] = progress_bar
            self.progress_bar_container_layout.addWidget(progress_bar)
            progress_bar.show()

            current_geometry = self.geometry()
            current_geometry.moveCenter(initial_geometry.center())
            self.setGeometry(current_geometry)
Beispiel #3
0
class GcodeBackplot(QBackPlot):
    line_selected = Signal(int)
    gcode_error = Signal(str)

    def __init__(self, parent=None, standalone=False):
        super(GcodeBackplot, self).__init__(parent)

        # This prevents doing unneeded initialization
        # when QtDesginer loads the plugin.
        if parent is None and not standalone:
            return

        self.show_overlay = False  # no DRO or DRO overlay
        self.program_alpha = True
        self.grid_size = 1
        self._reload_filename = None

        # Add loading progress bar and abort button
        self.progressBar = QProgressBar(visible=False)
        self.progressBar.setFormat("Loading backplot: %p%")
        self.abortButton = QPushButton('Abort', visible=False)

        hBox = QHBoxLayout()
        hBox.addWidget(self.progressBar)
        hBox.addWidget(self.abortButton)

        vBox = QVBoxLayout(self)
        vBox.addStretch()
        vBox.addLayout(hBox)

        self.abortButton.clicked.connect(self.abort)

        STATUS.actual_position.onValueChanged(self.update)
        STATUS.joint_actual_position.onValueChanged(self.update)
        STATUS.homed.onValueChanged(self.update)
        STATUS.limit.onValueChanged(self.update)
        STATUS.tool_in_spindle.onValueChanged(self.update)
        STATUS.motion_mode.onValueChanged(self.update)
        STATUS.current_vel.onValueChanged(self.update)

        STATUS.g5x_offset.onValueChanged(self.reloadBackplot)
        STATUS.g92_offset.onValueChanged(self.reloadBackplot)

        # Connect status signals
        STATUS.file.notify(self.loadBackplot)
        # STATUS.reload_backplot.notify(self.reloadBackplot)
        STATUS.program_units.notify(lambda v: self.setMetricUnits(v == 2))

    def loadBackplot(self, fname):
        LOG.debug('load the display: {}'.format(fname.encode('utf-8')))
        self._reload_filename = fname
        self.load(fname)

    @Slot()
    def reloadBackplot(self):
        QTimer.singleShot(100, lambda: self._reloadBackplot())

    def _reloadBackplot(self):
        LOG.debug('reload the display: {}'.format(self._reload_filename))
        dist = self.get_zoom_distance()
        try:
            self.load(self._reload_filename)
            self.set_zoom_distance(dist)
        except:
            LOG.warning("Problem reloading backplot file: {}".format(self._reload_filename), exc_info=True)

    # ==========================================================================
    #  Override QBackPlot methods
    # ==========================================================================

    def report_loading_started(self):
        self.progressBar.show()
        self.abortButton.show()
        self.start = time.time()

    def report_progress_percentage(self, percentage):
        QApplication.processEvents()
        self.progressBar.setValue(percentage)

    def report_loading_finished(self):
        print((time.time() - self.start))
        self.progressBar.hide()
        self.abortButton.hide()

    # overriding functions
    def report_gcode_error(self, result, seq, filename):
        error = gcode.strerror(result)
        file = os.path.basename(filename)
        line = seq - 1
        msg = "G-code error in '{}' near line {}: {}".format(file, line, error)
        LOG.error(msg)
        STATUS.backplot_gcode_error.emit(msg)

    # Override gremlin's / glcannon.py function so we can emit a GObject signal
    def update_highlight_variable(self, line):
        self.highlight_line = line
        if line is None:
            line = -1
        STATUS.backplot_line_selected.emit(line)

    # ==============================================================================
    #  QtDesigner property setters/getters
    # ==============================================================================

    @Slot(str)
    def setView(self, view):
        view = view.lower()
        if self.is_lathe:
            if view not in ['p', 'y', 'y2']:
                return False
        elif view not in ['p', 'x', 'y', 'z', 'z2']:
            return False
        self.current_view = view
        if self.initialised:
            self.set_current_view()

    def getView(self):
        return self.current_view

    defaultView = Property(str, getView, setView)

    @Slot()
    def setViewP(self):
        self.setView('p')

    @Slot()
    def setViewX(self):
        self.setView('x')

    @Slot()
    def setViewY(self):
        self.setView('y')

    @Slot()
    def setViewZ(self):
        self.setView('z')

    @Slot()
    def setViewZ2(self):
        self.setView('z2')

    @Slot()
    def clearLivePlot(self):
        self.clear_live_plotter()

    @Slot()
    def zoomIn(self):
        self.zoomin()

    @Slot()
    def zoomOut(self):
        self.zoomout()

    @Slot(bool)
    def alphaBlend(self, alpha):
        self.program_alpha = alpha
        self.update()

    @Slot(bool)
    def showGrid(self, grid):
        self.grid_size = int(grid)  # ugly hack for now
        self.update()

    # @Slot(str) Fixme check for the correct data type
    def setdro(self, state):
        self.enable_dro = state
        self.updateGL()

    def getdro(self):
        return self.enable_dro

    _dro = Property(bool, getdro, setdro)

    # DTG

    # @Slot(str) Fixme check for the correct data type
    def setdtg(self, state):
        self.show_dtg = state
        self.updateGL()

    def getdtg(self):
        return self.show_dtg

    _dtg = Property(bool, getdtg, setdtg)

    # METRIC

    # @Slot(str) Fixme check for the correct data type
    def setMetricUnits(self, metric):
        self.metric_units = metric
        self.updateGL()

    def getMetricUnits(self):
        return self.metric_units

    metricUnits = Property(bool, getMetricUnits, setMetricUnits)

    # @Slot(str) Fixme check for the correct data type
    def setProgramAlpha(self, alpha):
        self.program_alpha = alpha
        self.updateGL()

    def getProgramAlpha(self):
        return self.program_alpha

    renderProgramAlpha = Property(bool, getProgramAlpha, setProgramAlpha)

    # @Slot(str) Fixme check for the correct data type
    def setBackgroundColor(self, color):
        self.colors['back'] = color.getRgbF()[:3]
        self.updateGL()

    def getBackgroundColor(self):
        r, g, b = self.colors['back']
        color = QColor()
        color.setRgbF(r, g, b, 1.0)
        return color

    backgroundColor = Property(QColor, getBackgroundColor, setBackgroundColor)
Beispiel #4
0
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        uic.loadUi(f'{pathlib.Path(__file__).parent}/UI/mainwindow.ui', self)

        self.setWindowTitle('BlastSightDM')
        self.setWindowIcon(IconCollection.get('blastsight.png'))
        self.toolbar.setWindowTitle('Toolbar')

        self.setFocusPolicy(Qt.StrongFocus)
        self.setAcceptDrops(True)
        self.statusBar.showMessage('Ready')

        # Attributes
        self.filters_dict = {
            'mesh': {
                'load': 'Mesh Files (*.dxf *.off *.h5m);;'
                        'DXF Files (*.dxf);;'
                        'OFF Files (*.off);;'
                        'H5M Files (*.h5m);;'
                        'All Files (*.*)',
                'export': 'BlastSight Mesh (*.h5m);;'
                          'OFF File (*.off);;'
            },
            'block': {
                'load': 'Block Files (*.csv *.h5p *.out);;'
                        'CSV Files (*.csv);;'
                        'H5P Files (*.h5p);;'
                        'GSLib Files (*.out);;'
                        'All Files (*.*)',
                'export': 'BlastSight Blocks (*.h5p);;'
                          'CSV File (*.csv);;'
            },
            'point': {
                'load': 'Point Files (*.csv *.h5p *.out);;'
                        'CSV Files (*.csv);;'
                        'H5P Files (*.h5p);;'
                        'GSLib Files (*.out);;'
                        'All Files (*.*)',
                'export': 'BlastSight Points (*.h5p);;'
                          'CSV File (*.csv);;'
            },
            'line': {
                'load': 'Line Files (*.csv *.dxf);;'
                        'CSV Files (*.csv);;'
                        'DXF Files (*.dxf);;'
                        'All Files (*.*)',
                'export': 'CSV File (*.csv);;'
            },
            'tube': {
                'load': 'Line Files (*.csv *.dxf);;'
                        'CSV Files (*.csv);;'
                        'DXF Files (*.dxf);;'
                        'All Files (*.*)',
                'export': 'CSV File (*.csv);;'
            }
        }

        self.settings = QSettings('BlastSightDM', application='blastsight-dm', parent=self)

        # Progress bar (hidden by default)
        self.progress_bar = QProgressBar(self.statusBar)
        self.progress_bar.setValue(0)
        self.progress_bar.setMaximumWidth(self.width() / 5)
        self.progress_bar.hide()
        self.statusBar.addPermanentWidget(self.progress_bar)

        # Grid Widget
        self.grid_widget = GridWidget()
        self.grid_widget.connect_viewer(self.viewer)

        # XSection Widget
        self.xsection_widget = XSectionWidget()
        self.xsection_widget.connect_viewer(self.viewer)

        # Extra actions
        actions = self.toolbar.action_collection
        self.toolbar.addAction(actions.action_quit)
        self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)

        self.generate_menubar()
        self.connect_actions()

        # Set auto-fit as True when starting the app
        if not actions.action_autofit.isChecked():
            actions.action_autofit.trigger()

        # Set auto-rotate as True when starting the app
        if not actions.action_autorotate.isChecked():
            actions.action_autorotate.trigger()

        # Set animated as True when starting the app
        if not actions.action_animated.isChecked():
            actions.action_animated.trigger()

        # Set camera widget as hidden
        self.dockWidget_camera.hide()

    def generate_menubar(self) -> None:
        actions = self.toolbar.action_collection

        # File
        self.menu_File.addAction(actions.action_load_mesh)
        self.menu_File.addAction(actions.action_load_blocks)
        self.menu_File.addAction(actions.action_load_points)
        self.menu_File.addAction(actions.action_load_lines)
        self.menu_File.addAction(actions.action_load_tubes)
        self.menu_File.addSeparator()
        self.menu_File.addAction(actions.action_load_mesh_folder)
        self.menu_File.addAction(actions.action_load_blocks_folder)
        self.menu_File.addAction(actions.action_load_points_folder)
        self.menu_File.addAction(actions.action_load_lines_folder)
        self.menu_File.addAction(actions.action_load_tubes_folder)
        self.menu_File.addSeparator()
        self.menu_File.addAction(actions.action_quit)

        # View
        self.menu_View.addAction(actions.action_viewer_properties)
        self.menu_View.addAction(actions.action_plan_view)
        self.menu_View.addAction(actions.action_north_view)
        self.menu_View.addAction(actions.action_east_view)
        self.menu_View.addAction(actions.action_fit_to_screen)
        self.menu_View.addSeparator()
        self.menu_View.addAction(actions.action_perspective_projection)
        self.menu_View.addAction(actions.action_orthographic_projection)
        self.menu_View.addSeparator()
        self.menu_View.addAction(actions.action_grid)
        self.menu_View.addAction(actions.action_take_screenshot)

        # Tools
        self.menu_Tools.addAction(actions.action_slice_meshes)
        self.menu_Tools.addAction(actions.action_slice_blocks)
        self.menu_Tools.addAction(actions.action_slice_points)
        self.menu_Tools.addAction(actions.action_detection_controller)
        self.menu_Tools.addAction(actions.action_measurement_controller)
        self.menu_Tools.addSeparator()
        self.menu_Tools.addAction(actions.action_xsection)
        self.menu_Tools.addAction(actions.action_normal_controller)

        # Settings
        self.menu_Settings.addAction(actions.action_autofit)
        self.menu_Settings.addAction(actions.action_animated)
        self.menu_Settings.addAction(actions.action_autorotate)
        self.menu_Settings.addAction(actions.action_turbo_rendering)

        # Help
        self.menu_Help.addAction(actions.action_help)
        self.menu_Help.addAction(actions.action_about)

    def connect_actions(self) -> None:
        actions = self.toolbar.action_collection

        # Toolbar/Tree/Camera
        self.toolbar.connect_viewer(self.viewer)
        self.toolbar.connect_tree(self.dockWidget_tree)
        self.toolbar.connect_camera(self.dockWidget_camera)
        self.toolbar.connect_xsection(self.xsection_widget)
        self.toolbar.connect_grid(self.grid_widget)

        self.cameraWidget.connect_viewer(self.viewer)
        self.treeWidget.connect_viewer(self.viewer)
        self.treeWidget.enable_exportability(True)

        # File
        actions.action_load_mesh.triggered.connect(self.dialog_load_mesh)
        actions.action_load_blocks.triggered.connect(self.dialog_load_blocks)
        actions.action_load_points.triggered.connect(self.dialog_load_points)
        actions.action_load_lines.triggered.connect(self.dialog_load_lines)
        actions.action_load_tubes.triggered.connect(self.dialog_load_tubes)

        actions.action_load_mesh_folder.triggered.connect(self.dialog_load_mesh_folder)
        actions.action_load_blocks_folder.triggered.connect(self.dialog_load_blocks_folder)
        actions.action_load_points_folder.triggered.connect(self.dialog_load_points_folder)
        actions.action_load_lines_folder.triggered.connect(self.dialog_load_lines_folder)
        actions.action_load_tubes_folder.triggered.connect(self.dialog_load_tubes_folder)

        actions.action_quit.triggered.connect(self.close)

        # View
        actions.action_take_screenshot.triggered.connect(self.slot_screenshot)

        # Tools
        actions.action_slice_meshes.triggered.connect(self.slot_slice_meshes)
        actions.action_slice_blocks.triggered.connect(self.slot_slice_blocks)
        actions.action_slice_points.triggered.connect(self.slot_slice_points)
        actions.action_detection_controller.triggered.connect(self.slot_detection_controller)
        actions.action_measurement_controller.triggered.connect(self.slot_measurement_controller)

        actions.action_normal_controller.triggered.connect(self.slot_normal_controller)

        # Help
        actions.action_help.triggered.connect(self.slot_help)
        actions.action_about.triggered.connect(self.slot_about)

        # Viewer signals
        self.viewer.signal_fps_updated.connect(self.print_fps)
        self.viewer.signal_controller_updated.connect(self.slot_controller_updated)
        self.viewer.signal_elements_detected.connect(self.slot_elements_detected)
        self.viewer.signal_mesh_distances.connect(self.slot_mesh_distances)

        self.viewer.signal_load_success.connect(self.slot_element_load_success)
        self.viewer.signal_load_failure.connect(self.slot_element_load_failure)
        self.viewer.signal_export_success.connect(self.slot_element_export_success)
        self.viewer.signal_export_failure.connect(self.slot_element_export_failure)

        self.viewer.signal_process_updated.connect(self.slot_process_updated)
        self.viewer.signal_process_started.connect(self.slot_process_started)
        self.viewer.signal_process_finished.connect(self.slot_process_finished)

        # TreeWidget actions
        self.treeWidget.signal_export_element.connect(self._dialog_export_element)

    @property
    def last_dir(self) -> str:
        return self.settings.value('last_directory', '.')

    @last_dir.setter
    def last_dir(self, _last_dir: str) -> None:
        self.settings.setValue('last_directory', _last_dir)

    @staticmethod
    def print_fps(fps) -> None:
        print(f'               \r', end='')
        print(f'FPS: {fps:.1f} \r', end='')

    """
    Status bar updates
    """
    def slot_element_load_success(self, _id: int) -> None:
        self.statusBar.showMessage(f'Load successful (id: {_id}).')

    def slot_element_load_failure(self) -> None:
        self.statusBar.showMessage(f'Failed to load.')

    def slot_element_export_success(self, _id: int) -> None:
        self.statusBar.showMessage(f'Export successful (id: {_id}).')

    def slot_element_export_failure(self) -> None:
        self.statusBar.showMessage(f'Failed to export.')

    def slot_controller_updated(self, name: str) -> None:
        self.statusBar.showMessage(f'Controller: {name}')

    def slot_mesh_distances(self, distance_dict: dict) -> None:
        self.statusBar.showMessage(f'Distance: {distance_dict.get("distance")}')

    def slot_elements_detected(self, attributes: list) -> None:
        id_list = sorted(map(lambda attr: attr.get('id', -1), attributes))
        self.statusBar.showMessage(f'Detected elements: {id_list}')

    """
    Slots for slices
    """
    def handle_slices(self, description: dict, executer: callable) -> None:
        # Retrieve description vectors
        origin = description.get('origin')
        normal = description.get('normal')
        up = description.get('up')

        # Execute the callable over the elements
        executer(origin, normal)

        # Auto-rotate camera to meet cross-section
        actions = self.toolbar.action_collection
        if actions.action_autorotate.isChecked():
            self.viewer.set_camera_from_vectors(normal, up)

    def slot_slice_meshes(self) -> None:
        def executer(origin, normal) -> None:
            slices = self.viewer.slice_meshes(origin, normal)
            self.add_mesh_slices(slices)

        def handler(description: dict) -> None:
            self.handle_slices(description, executer)

            # Disconnect
            self.viewer.set_normal_controller()
            self.viewer.signal_slice_description.disconnect()

        self.viewer.set_slice_controller()
        self.viewer.signal_slice_description.connect(handler)

    def slot_slice_blocks(self) -> None:
        def executer(origin, normal) -> None:
            slices = self.viewer.slice_blocks(origin, normal)
            self.add_block_slices(slices)

        def handler(description: dict) -> None:
            self.handle_slices(description, executer)

            # Disconnect
            self.viewer.set_normal_controller()
            self.viewer.signal_slice_description.disconnect()

        self.viewer.set_slice_controller()
        self.viewer.signal_slice_description.connect(handler)

    def slot_slice_points(self) -> None:
        def executer(origin, normal) -> None:
            slices = self.viewer.slice_points(origin, normal)
            self.add_point_slices(slices)

        def handler(description: dict) -> None:
            self.handle_slices(description, executer)

            # Disconnect
            self.viewer.set_normal_controller()
            self.viewer.signal_slice_description.disconnect()

        self.viewer.set_slice_controller()
        self.viewer.signal_slice_description.connect(handler)

    def add_mesh_slices(self, slice_list: list) -> None:
        def add_slice(description: dict) -> None:
            slices = description.get('vertices')
            mesh_id = description.get('element_id')
            mesh = self.viewer.get_drawable(mesh_id)

            def add_subslice(subslice, num: int) -> None:
                self.viewer.lines(vertices=subslice,
                                  color=mesh.color,
                                  name=f'MESHSLICE_{num}_{mesh.name}',
                                  extension='csv',
                                  loop=True)

            # Execute add_subslice for all subslices of the slice
            for i, ssl in enumerate(slices):
                add_subslice(ssl, i)

        # Execute add_slice over all slice descriptions
        for sl in slice_list:
            add_slice(sl)

    def add_block_slices(self, slice_list: list) -> None:
        def add_slice(description: dict) -> None:
            indices = description.get('indices')
            block_id = description.get('element_id')
            block = self.viewer.get_drawable(block_id)

            if block.is_slice:
                return

            self.viewer.blocks(vertices=block.vertices[indices],
                               values=block.values[indices],
                               color=block.color[indices],
                               vmin=block.vmin,
                               vmax=block.vmax,
                               colormap=block.colormap,
                               block_size=block.block_size,
                               name=f'BLOCKSLICE_{block.name}',
                               extension='csv',
                               is_slice=True)

        # Execute add_slice over all slice descriptions
        for sl in slice_list:
            add_slice(sl)

    def add_point_slices(self, slice_list: list) -> None:
        def add_slice(description: dict) -> None:
            indices = description.get('indices')
            point_id = description.get('element_id')
            point = self.viewer.get_drawable(point_id)

            if point.is_slice:
                return

            self.viewer.points(vertices=point.vertices[indices],
                               values=point.values[indices],
                               color=point.color[indices],
                               vmin=point.vmin,
                               vmax=point.vmax,
                               colormap=point.colormap,
                               point_size=point.point_size,
                               name=f'POINTSLICE_{point.name}',
                               extension='csv',
                               is_slice=True)

        # Execute add_slice over all slice descriptions
        for sl in slice_list:
            add_slice(sl)

    """
    Common functionality for loading
    """
    @staticmethod
    def _thread_runner(method: callable, *args, **kwargs) -> None:
        worker = ThreadWorker(method, *args, **kwargs)
        QThreadPool.globalInstance().start(worker)

    def _dialog_load_element(self, loader: classmethod, hint: str, *args, **kwargs) -> None:
        (paths, selected_filter) = QFileDialog.getOpenFileNames(
            parent=self,
            directory=self.last_dir,
            filter=self.filters_dict.get(hint).get('load'))

        # If path == '', then bool(path) is False
        path_list = sorted(filter(bool, paths))

        if len(path_list) > 0:
            self.statusBar.showMessage(f'Loading {len(path_list)} element(s)...')
            self._thread_runner(self.viewer.load_multiple, path_list, loader, *args, **kwargs)
            self.last_dir = QFileInfo(path_list[-1]).absoluteDir().absolutePath()

    def _dialog_load_folder(self, loader: classmethod, *args, **kwargs) -> None:
        path = QFileDialog.getExistingDirectory(
            parent=self,
            directory=self.last_dir,
            options=QFileDialog.ShowDirsOnly)

        # Execute method
        if bool(path):
            self.statusBar.showMessage('Loading folder...')
            self._thread_runner(loader, path, *args, **kwargs)
            self.last_dir = path

    """
    Slots for progress updates
    """
    def slot_process_updated(self, value: int) -> None:
        self.progress_bar.setValue(value)

    def slot_process_started(self) -> None:
        self.progress_bar.setValue(0)
        self.progress_bar.show()

    def slot_process_finished(self,) -> None:
        self.progress_bar.hide()

    """
    Slot for extra items in view
    """
    def slot_screenshot(self) -> None:
        (path, selected_filter) = QFileDialog.getSaveFileName(
            parent=self.viewer,
            directory=f'BlastSight Screenshot ({datetime.now().strftime("%Y%m%d-%H%M%S")})',
            filter='PNG image (*.png);;')

        self.viewer.take_screenshot(path)

    """
    Slots for loading files
    """
    def dialog_load_mesh(self) -> None:
        self._dialog_load_element(loader=self.viewer.load_mesh, hint='mesh')

    def dialog_load_blocks(self) -> None:
        self._dialog_load_element(loader=self.viewer.load_blocks, hint='block')

    def dialog_load_points(self) -> None:
        self._dialog_load_element(loader=self.viewer.load_points, hint='point')

    def dialog_load_lines(self) -> None:
        self._dialog_load_element(loader=self.viewer.load_lines, hint='line')

    def dialog_load_tubes(self) -> None:
        self._dialog_load_element(loader=self.viewer.load_tubes, hint='tube')

    def dialog_load_mesh_folder(self) -> None:
        self._dialog_load_folder(loader=self.viewer.load_mesh_folder)

    def dialog_load_blocks_folder(self) -> None:
        self._dialog_load_folder(loader=self.viewer.load_blocks_folder)

    def dialog_load_points_folder(self) -> None:
        self._dialog_load_folder(loader=self.viewer.load_points_folder)

    def dialog_load_lines_folder(self) -> None:
        self._dialog_load_folder(loader=self.viewer.load_lines_folder)

    def dialog_load_tubes_folder(self) -> None:
        self._dialog_load_folder(loader=self.viewer.load_tubes_folder)

    """
    Slots for exporting elements
    """
    def _dialog_export_element(self, _id: int) -> None:
        element = self.viewer.get_drawable(_id).element
        proposed_path = QDir(self.last_dir).filePath(element.name)

        # TODO Check hints by element type
        filters = 'BlastSight Mesh (*.h5m);;'\
                  'BlastSight Blocks/Points (*.h5p);;'

        (path, selected_filter) = QFileDialog.getSaveFileName(
            parent=self,
            directory=proposed_path,
            filter=filters)

        # Execute method
        if bool(path):
            self.statusBar.showMessage('Exporting...')
            self._thread_runner(self.viewer.export_element, path, _id)

    """
    Slots for modifying interaction controllers
    """
    def slot_normal_controller(self) -> None:
        self.viewer.set_normal_controller()

    def slot_detection_controller(self) -> None:
        self.viewer.set_detection_controller()

    def slot_measurement_controller(self) -> None:
        self.viewer.set_measurement_controller()

    """
    Slots for showing help/about dialogs
    """
    def slot_help(self) -> None:
        HelpDialog(self).exec_()

    def slot_about(self) -> None:
        AboutDialog(self).exec_()

    """
    Events pass-through
    """
    def dragEnterEvent(self, event, *args, **kwargs) -> None:
        self.viewer.dragEnterEvent(event, *args, **kwargs)

    def dropEvent(self, event, *args, **kwargs) -> None:
        self.viewer.dropEvent(event, *args, **kwargs)