Example #1
0
        def load(path):
            LOG.info(
                "Loading global stylesheet: yellow<{}>".format(stylesheet))
            self.setStyleSheet("file:///" + path)

            if watch:
                from qtpy.QtCore import QFileSystemWatcher
                self.qss_file_watcher = QFileSystemWatcher()
                self.qss_file_watcher.addPath(stylesheet)
                self.qss_file_watcher.fileChanged.connect(load)
Example #2
0
    def initialise(self):
        """Start the periodic update timer."""

        # watch the gcode file for changes and reload as needed
        self.file_watcher = QFileSystemWatcher()
        self.file_watcher.addPath(self.file.value)
        self.file_watcher.fileChanged.connect(self.updateFile)

        LOG.debug("Starting periodic updates with %ims cycle time",
                  self._cycle_time)
        self.timer.start(self._cycle_time)
Example #3
0
    def __init__(self, parent, ide, path, dock_widget):

        super(FolderBrowser, self).__init__(parent)
        self.main_window = parent
        self._path = path
        self._ide = ide
        self._dock_widget = dock_widget
        self.clear_ignore_patterns()
        self.add_ignore_patterns(ide.ignore_patterns)
        self.set_root_path(os.path.normpath(path))
        self.set_context_menu(FileSystemContextMenu())
        self._watcher = QFileSystemWatcher()
        self._watcher.addPath(path)
        self._watcher.fileChanged.connect(self._index_files)
        self._watcher.directoryChanged.connect(self._index_files)
        self._indexing = False
        self._file_list = []
        self._index_files()
Example #4
0
    def __init__(self, parent, ide, path, dock_widget):

        super(FolderBrowser, self).__init__(parent)
        self.main_window = parent
        self._path = path
        self._ide = ide
        self._dock_widget = dock_widget
        self._dock_widget._gitignore_checkbox.stateChanged.connect(
            self._toggle_gitignore)
        self._set_ignore_patterns()
        self.set_root_path(os.path.normpath(path))
        self.set_context_menu(FileSystemContextMenu(self.main_window))
        self._watcher = QFileSystemWatcher()
        self._watcher.addPath(path)
        self._watcher.fileChanged.connect(self._on_file_changed)
        self._watcher.directoryChanged.connect(self._on_folder_changed)
        self._indexing = False
        self._file_list = []
        self._active = True
        self._timer = None
        self._index_files()
Example #5
0
class Status(DataPlugin):

    stat = STAT

    def __init__(self, cycle_time=100):
        super(Status, self).__init__()

        self.no_force_homing = INFO.noForceHoming()

        self.file_watcher = None

        # recent files
        self.max_recent_files = 10
        with RuntimeConfig('~/.axis_preferences') as rc:
            files = rc.get('DEFAULT', 'recentfiles', default=[])
        files = [file for file in files if os.path.exists(file)]
        self.recent_files.setValue(files)

        # MDI history
        self._max_mdi_history_length = 100
        self._mdi_history_file = INFO.getMDIHistoryFile()
        self.loadMdiHistory(self._mdi_history_file)

        self.jog_increment = 0  # jog
        self.step_jog_increment = INFO.getIncrements()[0]
        self.jog_mode = True
        self.linear_jog_velocity = INFO.getJogVelocity()
        self.angular_jog_velocity = INFO.getJogVelocity()

        try:
            STAT.poll()
        except:
            pass

        excluded_items = ['axis', 'joint', 'spindle', 'poll']

        self.old = {}
        # initialize data channels
        for item in dir(STAT):
            if item in self.channels:
                self.old[item] = getattr(STAT, item)
                self.channels[item].setValue(getattr(STAT, item))
            elif item not in excluded_items and not item.startswith('_'):
                self.old[item] = getattr(STAT, item)
                chan = DataChannel(doc=item)
                chan.setValue(getattr(STAT, item))
                self.channels[item] = chan
                setattr(self, item, chan)

        # add joint status channels
        self.joint = tuple(JointStatus(jnum) for jnum in range(9))
        for joint in self.joint:
            for chan, obj in joint.channels.items():
                self.channels['joint.{}.{}'.format(joint.jnum, chan)] = obj

        # add spindle status channels
        self.spindle = tuple(SpindleStatus(snum) for snum in range(8))
        for spindle in self.spindle:
            for chan, obj in spindle.channels.items():
                self.channels['spindle.{}.{}'.format(spindle.snum, chan)] = obj

        self.all_axes_homed.value = False
        self.homed.notify(self.all_axes_homed.setValue)
        self.enabled.notify(self.all_axes_homed.setValue)

        # Set up the periodic update timer
        self.timer = QTimer()
        self._cycle_time = cycle_time
        self.timer.timeout.connect(self._periodic)

        self.on.settable = True
        self.task_state.notify(
            lambda ts: self.on.setValue(ts == linuxcnc.STATE_ON))

    recent_files = DataChannel(doc='List of recently loaded files',
                               settable=True,
                               data=[])

    def loadMdiHistory(self, fname):
        """Load MDI history from file."""
        mdi_history = []
        if os.path.isfile(fname):
            with open(fname, 'r') as fh:
                for line in fh.readlines():
                    line = line.strip()
                    mdi_history.append(line)

        mdi_history.reverse()
        self.mdi_history.setValue(mdi_history)

    def saveMdiHistory(self, fname):
        """Write MDI history to file."""
        with open(fname, 'w') as fh:
            cmds = self.mdi_history.value
            cmds.reverse()
            for cmd in cmds:
                fh.write(cmd + '\n')

    @DataChannel
    def axis_mask(self, chan, format='int'):
        """Axes as configured in the [TRAJ]COORDINATES INI option.

        To return the string in a status label::

            status:axis_mask
            status:axis_mask?string
            status:axis_mask?list

        :returns: the configured axes
        :rtype: int, list, str
        """

        if format == 'list':

            mask = '{0:09b}'.format(self.stat.axis_mask or 7)

            axis_list = []
            for anum, enabled in enumerate(mask[::-1]):
                if enabled == '1':
                    axis_list.append(anum)

            return axis_list

        return self.stat.axis_mask

    @axis_mask.tostring
    def axis_mask(self, chan):
        axes = ''
        for anum in self.axis_mask.getValue(format='list'):
            axes += 'XYZABCUVW'[anum]

        return axes

    @DataChannel
    def mdi_history(self, chan):
        """List of recently issued MDI commands.
            Commands are stored in reverse chronological order, with the
            newest command at the front of the list, and oldest at the end.
            When the list exceeds the length given by MAX_MDI_COMMANDS the
            oldest entries will be dropped.

            Duplicate commands will not be removed, so that MDI History
            can be replayed via the queue meachanisim from a point in
            the history forward. The most recently issued
            command will always be at the front of the list.
        """
        return chan.value

    @mdi_history.setter
    def mdi_history(self, chan, new_value):
        LOG.debug("---------set mdi_history: {}, {}".format(chan, new_value))
        if isinstance(new_value, list):
            chan.value = new_value[:self._max_mdi_history_length]
        else:
            cmd = str(new_value.strip())
            cmds = chan.value
            LOG.debug("---------cmd: {}".format(cmd))
            LOG.debug("---------cmds: {}".format(cmds))
            if cmd in cmds:
                cmds.remove(cmd)

            cmds.insert(0, cmd)
            chan.value = cmds[:self._max_mdi_history_length]
            LOG.debug("---------chan.value: {}".format(chan.value))

        chan.signal.emit(chan.value)

    def mdi_remove_entry(self, mdi_index):
        """Remove the indicated cmd by index reference"""
        # TODO: This has some potential code redundancy. Follow above pattern
        chan = self.mdi_history
        cmds = chan.value
        del cmds[mdi_index]
        chan.signal.emit(cmds)

    def mdi_swap_entries(self, index1, index2):
        """Swicth two entries about."""
        chan = self.mdi_history
        cmds = chan.value
        cmds[index2], cmds[index1] = cmds[index1], cmds[index2]
        chan.signal.emit(cmds)

    @DataChannel
    def on(self, chan):
        """True if machine power is ON."""
        return STAT.task_state == linuxcnc.STATE_ON

    @DataChannel
    def file(self, chan):
        """Currently loaded file including path"""
        return chan.value or 'No file loaded'

    @file.setter
    def file(self, chan, fname):
        if STAT.interp_state == linuxcnc.INTERP_IDLE \
                and STAT.call_level == 0:

            if self.file_watcher is not None:
                if self.file_watcher.files():
                    self.file_watcher.removePath(chan.value)
                if os.path.isfile(fname):
                    self.file_watcher.addPath(fname)

            chan.value = fname
            chan.signal.emit(fname)

    def updateFile(self, path):
        if STAT.interp_state == linuxcnc.INTERP_IDLE:
            LOG.debug("Reloading edited G-Code file: %s", path)
            if os.path.isfile(path):
                self.file.signal.emit(path)
                CMD.program_open(path)
        else:
            LOG.debug("G-Code file changed, won't reload: %s", path)

    @DataChannel
    def state(self, chan):
        """Current command execution status

        1) Done
        2) Exec
        3) Error

        To return the string in a status label::

            status:state?string

        :returns: current command execution state
        :rtype: int, str
        """
        return STAT.state

    @state.tostring
    def state(self, chan):
        states = {
            0: "N/A",
            linuxcnc.RCS_DONE: "Done",
            linuxcnc.RCS_EXEC: "Exec",
            linuxcnc.RCS_ERROR: "Error"
        }

        return states[STAT.state]

    @DataChannel
    def exec_state(self, chan):
        """Current task execution state

        1) Error
        2) Done
        3) Waiting for Motion
        4) Waiting for Motion Queue
        5) Waiting for Pause
        6) --
        7) Waiting for Motion and IO
        8) Waiting for Delay
        9) Waiting for system CMD
        10) Waiting for spindle orient

        To return the string in a status label::

            status:exec_state?string

        :returns: current task execution error
        :rtype: int, str
        """
        return STAT.exec_state

    @exec_state.tostring
    def exec_state(self, chan):
        exec_states = {
            0:
            "N/A",
            linuxcnc.EXEC_ERROR:
            "Error",
            linuxcnc.EXEC_DONE:
            "Done",
            linuxcnc.EXEC_WAITING_FOR_MOTION:
            "Waiting for Motion",
            linuxcnc.EXEC_WAITING_FOR_MOTION_QUEUE:
            "Waiting for Motion Queue",
            linuxcnc.EXEC_WAITING_FOR_IO:
            "Waiting for Pause",
            linuxcnc.EXEC_WAITING_FOR_MOTION_AND_IO:
            "Waiting for Motion and IO",
            linuxcnc.EXEC_WAITING_FOR_DELAY:
            "Waiting for Delay",
            linuxcnc.EXEC_WAITING_FOR_SYSTEM_CMD:
            "Waiting for system CMD",
            linuxcnc.EXEC_WAITING_FOR_SPINDLE_ORIENTED:
            "Waiting for spindle orient"
        }

        return exec_states[STAT.exec_state]

    @DataChannel
    def interp_state(self, chan):
        """Current state of RS274NGC interpreter

        1) Idle
        2) Reading
        3) Paused
        4) Waiting

        To return the string in a status label::

            status:interp_state?string

        :returns: RS274 interpreter state
        :rtype: int, str
        """
        return STAT.interp_state

    @interp_state.tostring
    def interp_state(self, chan):
        interp_states = {
            0: "N/A",
            linuxcnc.INTERP_IDLE: "Idle",
            linuxcnc.INTERP_READING: "Reading",
            linuxcnc.INTERP_PAUSED: "Paused",
            linuxcnc.INTERP_WAITING: "Waiting"
        }

        return interp_states[STAT.interp_state]

    @DataChannel
    def interpreter_errcode(self, chan):
        """Current RS274NGC interpreter return code

        0) Ok
        1) Exit
        2) Finished
        3) Endfile
        4) File not open
        5) Error

        To return the string in a status label::

            status:interpreter_errcode?string

        :returns: interp error code
        :rtype: int, str
        """
        return STAT.interpreter_errcode

    @interpreter_errcode.tostring
    def interpreter_errcode(self, chan):
        interpreter_errcodes = {
            0: "Ok",
            1: "Exit",
            2: "Finished",
            3: "Endfile",
            4: "File not open",
            5: "Error"
        }

        return interpreter_errcodes[STAT.interpreter_errcode]

    @DataChannel
    def task_state(self, chan, query=None):
        """Current status of task

        1) E-Stop
        2) Reset
        3) Off
        4) On

        To return the string in a status label::

            status:task_state?string

        :returns: current task state
        :rtype: int, str
        """
        return STAT.task_state

    @task_state.tostring
    def task_state(self, chan):
        task_states = {
            0: "N/A",
            linuxcnc.STATE_ESTOP: "E-Stop",
            linuxcnc.STATE_ESTOP_RESET: "Reset",
            linuxcnc.STATE_ON: "On",
            linuxcnc.STATE_OFF: "Off"
        }

        return task_states[STAT.task_state]

    @DataChannel
    def task_mode(self, chan):
        """Current task mode

        1) Manual
        2) Auto
        3) MDI

        To return the string in a status label::

            status:task_mode?string

        :returns: current task mode
        :rtype: int, str
        """
        return STAT.task_mode

    @task_mode.tostring
    def task_mode(self, chan):
        task_modes = {
            0: "N/A",
            linuxcnc.MODE_MANUAL: "Manual",
            linuxcnc.MODE_AUTO: "Auto",
            linuxcnc.MODE_MDI: "MDI"
        }

        return task_modes[STAT.task_mode]

    @DataChannel
    def motion_mode(self, chan):
        """Current motion controller mode

        1) Free
        2) Coord
        3) Teleop

        To return the string in a status label::

            status:motion_mode?string

        :returns: current motion mode
        :rtype: int, str
        """
        return STAT.motion_mode

    @motion_mode.tostring
    def motion_mode(self, chan):
        modes = {
            0: "N/A",
            linuxcnc.TRAJ_MODE_COORD: "Coord",
            linuxcnc.TRAJ_MODE_FREE: "Free",
            linuxcnc.TRAJ_MODE_TELEOP: "Teleop"
        }

        return modes[STAT.motion_mode]

    @DataChannel
    def motion_type(self, chan, query=None):
        """Motion type

        0) None
        1) Traverse
        2) Linear Feed
        3) Arc Feed
        4) Tool Change
        5) Probing
        6) Rotary Index

        To return the string in a status label::

            status:motion_type?string

        :returns:  current motion type
        :rtype: int, str
        """
        return STAT.motion_type

    @motion_type.tostring
    def motion_type(self, chan):
        motion_types = {
            0: "None",
            linuxcnc.MOTION_TYPE_TRAVERSE: "Traverse",
            linuxcnc.MOTION_TYPE_FEED: "Linear Feed",
            linuxcnc.MOTION_TYPE_ARC: "Arc Feed",
            linuxcnc.MOTION_TYPE_TOOLCHANGE: "Tool Change",
            linuxcnc.MOTION_TYPE_PROBING: "Probing",
            linuxcnc.MOTION_TYPE_INDEXROTARY: "Rotary Index"
        }

        return motion_types[STAT.motion_type]

    @DataChannel
    def program_units(self, chan):
        """Program units

        Available as an integer, or in short or long string formats.

        1) in, Inches
        2) mm, Millimeters
        3) cm, Centimeters

        To return the string in a status label::

            status:program_units
            status:program_units?string
            status:program_units?string&format=long

        :returns: current program units
        :rtype: int, str
        """
        return STAT.program_units

    @program_units.tostring
    def program_units(self, chan, format='short'):
        if format == 'short':
            return ["N/A", "in", "mm", "cm"][STAT.program_units]
        else:
            return ["N/A", "Inches", "Millimeters",
                    "Centimeters"][STAT.program_units]

    @DataChannel
    def linear_units(self, chan):
        """Machine linear units

        Available as float (units/mm), or in short or long string formats.

        To return the string in a status label::

            status:linear_units
            status:linear_units?string
            status:linear_units?string&format=long

        :returns: machine linear units
        :rtype: float, str
        """
        return STAT.linear_units

    @linear_units.tostring
    def linear_units(self, chan, format='short'):
        if format == 'short':
            return {0.0: "N/A", 1.0: "mm", 1 / 25.4: "in"}[STAT.linear_units]
        else:
            return {
                0.0: "N/A",
                1.0: "Millimeters",
                1 / 25.4: "Inches"
            }[STAT.linear_units]

    @DataChannel
    def gcodes(self, chan, fmt=None):
        """G-codes

        active G-codes for each modal group

        | syntax ``status:gcodes`` returns tuple of strings
        | syntax ``status:gcodes?raw`` returns tuple of integers
        | syntax ``status:gcodes?string`` returns str
        """
        if fmt == 'raw':
            return STAT.gcodes
        return chan.value

    @gcodes.tostring
    def gcodes(self, chan):
        return " ".join(chan.value)

    @gcodes.setter
    def gcodes(self, chan, gcodes):
        chan.value = tuple(
            ["G%g" % (c / 10.) for c in sorted(gcodes[1:]) if c != -1])
        chan.signal.emit(self.gcodes.value)

    @DataChannel
    def mcodes(self, chan, fmt=None):
        """M-codes

        active M-codes for each modal group

        | syntax ``status:mcodes`` returns tuple of strings
        | syntax ``status:mcodes?raw`` returns tuple of integers
        | syntax ``status:mcodes?string`` returns str
        """
        if fmt == 'raw':
            return STAT.mcodes
        return chan.value

    @mcodes.tostring
    def mcodes(self, chan):
        return " ".join(chan.value)

    @mcodes.setter
    def mcodes(self, chan, gcodes):
        chan.value = tuple(
            ["M%g" % gcode for gcode in sorted(gcodes[1:]) if gcode != -1])
        chan.signal.emit(chan.value)

    @DataChannel
    def g5x_index(self, chan):
        """Current G5x work coord system

        | syntax ``status:g5x_index`` returns int
        | syntax ``status:g5x_index?string`` returns str
        """
        return STAT.g5x_index

    @g5x_index.tostring
    def g5x_index(self, chan):
        return [
            "G53", "G54", "G55", "G56", "G57", "G58", "G59", "G59.1", "G59.2",
            "G59.3"
        ][STAT.g5x_index]

    @DataChannel
    def settings(self, chan, item=None):
        """Interpreter Settings

        Available Items:
            0) sequence_number
            1) feed
            2) speed

        :return: interpreter settings
        :rtype: tuple, int, float
        """
        if item is None:
            return STAT.settings
        return STAT.settings[{
            'sequence_number': 0,
            'feed': 1,
            'speed': 2
        }[item]]

    @DataChannel
    def homed(self, chan, anum=None):
        """Axis homed status

        If no axis number is specified returns a tuple of integers.
        If ``anum`` is specified returns True if the axis is homed, else False.

        Rules syntax::

            status:homed
            status:homed?anum=0

        Args:
         anum (int, optional) : the axis number to return the homed state of.

        :returns: axis homed states
        :rtype: tuple, bool

        """
        if anum is None:
            return STAT.homed
        return bool(STAT.homed[int(anum)])

    @DataChannel
    def all_axes_homed(self, chan):
        """All axes homed status

        True if all axes are homed or if [TRAJ]NO_FORCE_HOMING set in INI.

        If [TRAJ]NO_FORCE_HOMING is set in the INI the value will be come
        true as soon as the machine is turned on and the signal will be emitted,
        otherwise the signal will be emitted once all the axes defined in the
        INI have been homed.

        :returns: all homed
        :rtype: bool
        """
        return chan.value

    @all_axes_homed.setter
    def all_axes_homed(self, chan, homed):
        if self.no_force_homing:
            all_homed = True
        else:
            for anum in INFO.AXIS_NUMBER_LIST:
                if STAT.homed[anum] is not 1:
                    all_homed = False
                    break
            else:
                all_homed = True

        if all_homed != chan.value:
            chan.value = all_homed
            chan.signal.emit(chan.value)

    # this is used by File "qtpyvcp/qtpyvcp/actions/program_actions.py",
    # line 83, in _run_ok elif not STATUS.allHomed():

    def allHomed(self):
        if self.no_force_homing:
            return True
        for jnum in range(STAT.joints):
            if not STAT.joint[jnum]['homed']:
                return False
        return True

    def forceUpdateStaticChannelMembers(self):
        """Static items need a force update to operate properly with the
        gui rules.  This needs to be done with consideration to the
        data structure so as to not "break" things.
        """
        # TODO: add to this list as needed. Possible to externalise via yaml?
        self.old['axes'] = None

    def initialise(self):
        """Start the periodic update timer."""

        # watch the gcode file for changes and reload as needed
        self.file_watcher = QFileSystemWatcher()
        if self.file.value:
            self.file_watcher.addPath(self.file.value)
        self.file_watcher.fileChanged.connect(self.updateFile)

        LOG.debug("Starting periodic updates with %ims cycle time",
                  self._cycle_time)
        self.timer.start(self._cycle_time)

        self.forceUpdateStaticChannelMembers()

    def terminate(self):
        """Save persistent data on terminate."""

        # save recent files
        with RuntimeConfig('~/.axis_preferences') as rc:
            rc.set('DEFAULT', 'recentfiles', self.recent_files.value)

        # save MDI history
        self.saveMdiHistory(self._mdi_history_file)

    def _periodic(self):

        # s = time.time()

        try:
            STAT.poll()
        except Exception:
            LOG.warning("Status polling failed, is LinuxCNC running?",
                        exc_info=True)
            self.timer.stop()
            return

        # status updates
        for item, old_val in self.old.iteritems():
            new_val = getattr(STAT, item)
            if new_val != old_val:
                self.old[item] = new_val
                self.channels[item].setValue(new_val)

        # joint status updates
        for joint in self.joint:
            joint._update()

        # spindle status updates
        for spindle in self.spindle:
            spindle._update()
Example #6
0
 def initialise(self):
     self.fs_watcher = QFileSystemWatcher()
     self.fs_watcher.addPath(self.tool_table_file)
     self.fs_watcher.fileChanged.connect(self.onToolTableFileChanged)
Example #7
0
class ToolTable(DataPlugin):

    TOOL_TABLE = {0: NO_TOOL}
    DEFAULT_TOOL = DEFAULT_TOOL
    COLUMN_LABELS = COLUMN_LABELS

    tool_table_changed = Signal(dict)

    def __init__(self, columns='TPXYZDR', file_header_template=None):
        super(ToolTable, self).__init__()

        self.fs_watcher = None
        self.orig_header_lines = []
        self.file_header_template = file_header_template or ''
        self.columns = self.validateColumns(columns) or [c for c in 'TPXYZDR']

        self.setCurrentToolNumber(0)

        self.tool_table_file = INFO.getToolTableFile()
        if not os.path.exists(self.tool_table_file):
            return

        self.loadToolTable()

        self.current_tool.setValue(
            self.TOOL_TABLE[STATUS.tool_in_spindle.getValue()])

        # update signals
        STATUS.tool_in_spindle.notify(self.setCurrentToolNumber)
        STATUS.tool_table.notify(lambda *args: self.loadToolTable())

    @DataChannel
    def current_tool(self, chan, item=None):
        """Current Tool Info

        Available items:

        * T -- tool number
        * P -- pocket number
        * X -- x offset
        * Y -- y offset
        * Z -- z offset
        * A -- a offset
        * B -- b offset
        * C -- c offset
        * U -- u offset
        * V -- v offset
        * W -- w offset
        * I -- front angle
        * J -- back angle
        * Q -- orientation
        * R -- remark

        Rules channel syntax::

            tooltable:current_tool
            tooltable:current_tool?X
            tooltable:current_tool?x_offset

        :param item: the name of the tool data item to get
        :return: dict, int, float, str
        """
        if item is None:
            return self.TOOL_TABLE[STAT.tool_in_spindle]
        return self.TOOL_TABLE[STAT.tool_in_spindle].get(item[0].upper())

    def initialise(self):
        self.fs_watcher = QFileSystemWatcher()
        self.fs_watcher.addPath(self.tool_table_file)
        self.fs_watcher.fileChanged.connect(self.onToolTableFileChanged)

    @staticmethod
    def validateColumns(columns):
        """Validate display column specification.

        The user can specify columns in multiple ways, method is used to make
        sure that that data is validated and converted to a consistent format.

        Args:
            columns (str | list) : A string or list of the column IDs
                that should be shown in the tooltable.

        Returns:
            None if not valid, else a list of uppercase column IDs.
        """
        if not isinstance(columns, (basestring, list, tuple)):
            return

        return [
            col for col in [col.strip().upper() for col in columns]
            if col in 'TPXYZABCUVWDIJQR' and not col == ''
        ]

    def newTool(self, tnum=None):
        """Get a dict of default tool values for a new tool."""
        if tnum is None:
            tnum = len(self.TOOL_TABLE)
        new_tool = DEFAULT_TOOL.copy()
        new_tool.update({'T': tnum, 'P': tnum, 'R': 'New Tool'})
        return new_tool

    def onToolTableFileChanged(self, path):
        LOG.debug('Tool Table file changed: {}'.format(path))
        # ToolEdit deletes the file and then rewrites it, so wait
        # a bit to ensure the new data has been writen out.
        QTimer.singleShot(50, self.reloadToolTable)

    def setCurrentToolNumber(self, tool_num):
        self.current_tool.setValue(self.TOOL_TABLE[tool_num])

    def reloadToolTable(self):
        # rewatch the file if it stop being watched because it was deleted
        if self.tool_table_file not in self.fs_watcher.files():
            self.fs_watcher.addPath(self.tool_table_file)

        # reload with the new data
        tool_table = self.loadToolTable()
        self.tool_table_changed.emit(tool_table)

    def iterTools(self, tool_table=None, columns=None):
        tool_table = tool_table or self.TOOL_TABLE
        columns = self.validateColumns(columns) or self.columns
        for tool in sorted(tool_table.iterkeys()):
            tool_data = tool_table[tool]
            yield [tool_data[key] for key in columns]

    def loadToolTable(self, tool_file=None):

        if tool_file is None:
            tool_file = self.tool_table_file

        if not os.path.exists(tool_file):
            if IN_DESIGNER:
                lorum_tooltable = makeLorumIpsumToolTable()
                self.current_tool.setValue(lorum_tooltable)
                return lorum_tooltable
            LOG.critical(
                "Tool table file does not exist: {}".format(tool_file))
            return {}

        with open(tool_file, 'r') as fh:
            lines = [line.strip() for line in fh.readlines()]

        # find opening colon, and get header data so it can be restored
        for rlnum, line in enumerate(reversed(lines)):
            if line.startswith(';'):
                lnum = len(lines) - rlnum
                raw_header = lines[:lnum]
                lines = lines[lnum:]

                self.orig_header_lines = list(
                    takewhile(
                        lambda l: not l.strip() == '---' and not l.startswith(
                            ';Tool'), raw_header))
                break

        table = {
            0: NO_TOOL,
        }
        for line in lines:

            data, sep, comment = line.partition(';')

            tool = DEFAULT_TOOL.copy()
            for item in data.split():
                descriptor = item[0]
                if descriptor in 'TPXYZABCUVWDIJQR':
                    value = item.lstrip(descriptor)
                    if descriptor in ('T', 'P', 'Q'):
                        try:
                            tool[descriptor] = int(value)
                        except:
                            LOG.error(
                                'Error converting value to int: {}'.format(
                                    value))
                            break
                    else:
                        try:
                            tool[descriptor] = float(value)
                        except:
                            LOG.error(
                                'Error converting value to float: {}'.format(
                                    value))
                            break

            tool['R'] = comment.strip()

            tnum = tool['T']
            if tnum == -1:
                continue

            # add the tool to the table
            table[tnum] = tool

        # update tooltable
        self.__class__.TOOL_TABLE = table

        self.current_tool.setValue(
            self.TOOL_TABLE[STATUS.tool_in_spindle.getValue()])

        # import json
        # print json.dumps(table, sort_keys=True, indent=4)

        # self.tool_table_changed.emit(table)
        return table.copy()

    def getToolTable(self):
        return self.TOOL_TABLE.copy()

    def saveToolTable(self, tool_table, columns=None, tool_file=None):
        """Write tooltable data to file.

        Args:
            tool_table (dict) : Dictionary of dictionaries containing
                the tool data to write to the file.
            columns (str | list) : A list of data columns to write.
                If `None` will use the value of ``self.columns``.
            tool_file (str) : Path to write the tooltable too.
                Defaults to ``self.tool_table_file``.
        """

        columns = self.validateColumns(columns) or self.columns

        if tool_file is None:
            tool_file = self.tool_table_file

        lines = []
        header_lines = []

        # restore file header
        if self.file_header_template:
            try:
                header_lines = self.file_header_template.format(
                    version=qtpyvcp.__version__,
                    datetime=datetime.now()).lstrip().splitlines()
                header_lines.append('')  # extra new line before table header
            except:
                pass

        if self.orig_header_lines:
            try:
                self.orig_header_lines.extend(
                    header_lines[header_lines.index('---'):])
                header_lines = self.orig_header_lines
            except ValueError:
                header_lines = self.orig_header_lines

        lines.extend(header_lines)

        # create the table header
        items = []
        for col in columns:
            if col == 'R':
                continue
            w = (6 if col in 'TPQ' else 8) - 1 if col == self.columns[0] else 0
            items.append('{:<{w}}'.format(COLUMN_LABELS[col], w=w))

        items.append('Remark')
        lines.append(';' + ' '.join(items))

        # add the tools
        for tool_num in sorted(tool_table.iterkeys())[1:]:
            items = []
            tool_data = tool_table[tool_num]
            for col in columns:
                if col == 'R':
                    continue
                items.append('{col}{val:<{w}}'.format(
                    col=col, val=tool_data[col], w=6 if col in 'TPQ' else 8))

            comment = tool_data.get('R', '')
            if comment is not '':
                items.append('; ' + comment)

            lines.append(''.join(items))

        # for line in lines:
        #     print line

        # write to file
        with open(tool_file, 'w') as fh:
            fh.write('\n'.join(lines))
            fh.write('\n')  # new line at end of file
            fh.flush()
            os.fsync(fh.fileno())

        CMD.load_tool_table()
Example #8
0
    def __init__(self):
        super().__init__()
        
        self.settings = Settings()
        
        self._saveLabel = QLabel()
        self._summaryLabel = QLabel()
        self.statusBar().addWidget(self._saveLabel)
        self.statusBar().addWidget(self._summaryLabel)
        
        self.file = self.getFile()
        self.sep = ','
        if not os.path.exists(self.file):
            header = ['Date', 'Time', 'Distance (km)', 'Calories', 'Gear']
            s = self.sep.join(header)
            with open(self.file, 'w') as fileobj:
                fileobj.write(s+'\n')
                
        df = pd.read_csv(self.file, sep=self.sep, parse_dates=['Date'])
        self.data = CycleData(df)
        self.save()
        self.dataAnalysis = CycleDataAnalysis(self.data)
        
        self.summary = Summary()

        numTopSessions = self.settings.value("pb/numSessions", 5, int)
        monthCriterion = self.settings.value("pb/bestMonthCriterion", "distance")
        self.pb = PersonalBests(self, numSessions=numTopSessions, 
                                monthCriterion=monthCriterion)
        self.viewer = CycleDataViewer(self)
        self.addData = AddCycleData()
        plotStyle = self.settings.value("plot/style", "dark")
        self.plot = CyclePlotWidget(self, style=plotStyle)
        
        self.pb.bestMonth.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
        self.pb.bestSessions.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
        self.addData.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
        
        self.summary.valueChanged.connect(self.viewer.newData)
        self.summary.valueChanged.connect(self.pb.newData)
        self.addData.newData.connect(self.data.append)
        self.data.dataChanged.connect(self.viewer.newData)
        self.data.dataChanged.connect(self.plot.newData)
        self.data.dataChanged.connect(self.pb.newData)
        self.data.dataChanged.connect(self.save)
        self.plot.pointSelected.connect(self.viewer.highlightItem)
        self.viewer.itemSelected.connect(self.plot.setCurrentPointFromDate)
        self.viewer.selectedSummary.connect(self._summaryLabel.setText)
        self.pb.itemSelected.connect(self.plot.setCurrentPointFromDate)
        self.pb.numSessionsChanged.connect(self.setPbSessionsDockLabel)
        self.pb.monthCriterionChanged.connect(self.setPbMonthDockLabel)
        
        self.fileChangedTimer = QTimer()
        self.fileChangedTimer.setInterval(100)
        self.fileChangedTimer.setSingleShot(True)
        self.fileChangedTimer.timeout.connect(self.csvFileChanged)
        self.fileWatcher = QFileSystemWatcher([self.file])
        self.fileWatcher.fileChanged.connect(self.startTimer)
        
        
        dockWidgets = [(self.pb.bestMonth, Qt.LeftDockWidgetArea, 
                        f"Best month ({monthCriterion})", "PB month"),
                       (self.pb.bestSessions, Qt.LeftDockWidgetArea, 
                        f"Top {intToStr(numTopSessions)} sessions", "PB sessions"),
                       (self.viewer, Qt.LeftDockWidgetArea, "Monthly data"),
                       (self.addData, Qt.LeftDockWidgetArea, "Add data")]
        
        for args in dockWidgets:
            self.createDockWidget(*args)
        self.setCentralWidget(self.plot)
        
        state = self.settings.value("window/state", None)
        if state is not None:
            self.restoreState(state)
            
        geometry = self.settings.value("window/geometry", None)    
        if geometry is not None:
            self.restoreGeometry(geometry)
        
        self.prefDialog = PreferencesDialog(self)
        
        self.createActions()
        self.createMenus()
        
        fileDir = os.path.split(__file__)[0]
        path = os.path.join(fileDir, "..", "images/icon.png")
        icon = QIcon(path)
        self.setWindowIcon(icon)
Example #9
0
class Status(DataPlugin):

    stat = STAT

    def __init__(self, cycle_time=100):
        super(Status, self).__init__()

        self.no_force_homing = INFO.noForceHoming()

        self.file_watcher = None

        self.max_recent_files = PREFS.getPref("STATUS", "MAX_RECENT_FILES", 10,
                                              int)
        files = PREFS.getPref("STATUS", "RECENT_FILES", [], list)
        self.recent_files = [file for file in files if os.path.exists(file)]

        self.jog_increment = 0  # jog
        self.step_jog_increment = INFO.getIncrements()[0]
        self.jog_mode = True
        self.linear_jog_velocity = INFO.getJogVelocity()
        self.angular_jog_velocity = INFO.getJogVelocity()

        try:
            STAT.poll()
        except:
            pass

        excluded_items = [
            'axis', 'joint', 'spindle', 'poll', 'command', 'debug'
        ]

        self.old = {}
        # initialize data channels
        for item in dir(STAT):
            if item in self.channels:
                self.old[item] = getattr(STAT, item)
                self.channels[item].setValue(getattr(STAT, item))
            elif item not in excluded_items and not item.startswith('_'):
                self.old[item] = getattr(STAT, item)
                chan = DataChannel(doc=item)
                chan.setValue(getattr(STAT, item))
                self.channels[item] = chan
                setattr(self, item, chan)

        # add joint status channels
        self.joint = tuple(JointStatus(jnum) for jnum in range(9))
        for joint in self.joint:
            for chan, obj in joint.channels.items():
                self.channels['joint.{}.{}'.format(joint.jnum, chan)] = obj

        # add spindle status channels
        self.spindle = tuple(SpindleStatus(snum) for snum in range(8))
        for spindle in self.spindle:
            for chan, obj in spindle.channels.items():
                self.channels['spindle.{}.{}'.format(spindle.snum, chan)] = obj

        self.all_axes_homed.setValue(STAT.homed)
        self.homed.notify(self.all_axes_homed.setValue)

        # Set up the periodic update timer
        self.timer = QTimer()
        self._cycle_time = cycle_time
        self.timer.timeout.connect(self._periodic)

        self.on.settable = True
        self.task_state.notify(
            lambda ts: self.on.setValue(ts == linuxcnc.STATE_ON))

    recent_files = DataChannel(doc='List of recently loaded files',
                               settable=True,
                               data=[])

    @DataChannel
    def on(self, chan):
        """True if machine power is ON."""
        return STAT.task_state == linuxcnc.STATE_ON

    @DataChannel
    def file(self, chan):
        """Currently loaded file including path"""
        return chan.value or 'No file loaded'

    @file.setter
    def file(self, chan, fname):
        if STAT.interp_state == linuxcnc.INTERP_IDLE \
                and STAT.call_level == 0:

            if self.file_watcher is not None:
                self.file_watcher.removePath(chan.value)
                self.file_watcher.addPath(fname)

            chan.value = fname
            chan.signal.emit(fname)

    def updateFile(self, path):
        LOG.debug("Reloading edited G-Code file: %s", path)
        self.file.signal.emit(path)
        CMD.program_open(path)

    @DataChannel
    def state(self, chan):
        """Current command execution status

        1) Done
        2) Exec
        3) Error

        To return the string in a status label::

            status:state?string

        :returns: current command execution state
        :rtype: int, str
        """
        return STAT.state

    @state.tostring
    def state(self, chan):
        states = {
            0: "N/A",
            linuxcnc.RCS_DONE: "Done",
            linuxcnc.RCS_EXEC: "Exec",
            linuxcnc.RCS_ERROR: "Error"
        }

        return states[STAT.state]

    @DataChannel
    def exec_state(self, chan):
        """Current task execution state

        1) Error
        2) Done
        3) Waiting for Motion
        4) Waiting for Motion Queue
        5) Waiting for Pause
        6) --
        7) Waiting for Motion and IO
        8) Waiting for Delay
        9) Waiting for system CMD
        10) Waiting for spindle orient

        To return the string in a status label::

            status:exec_state?string

        :returns: current task execution error
        :rtype: int, str
        """
        return STAT.exec_state

    @exec_state.tostring
    def exec_state(self, chan):
        exec_states = {
            0:
            "N/A",
            linuxcnc.EXEC_ERROR:
            "Error",
            linuxcnc.EXEC_DONE:
            "Done",
            linuxcnc.EXEC_WAITING_FOR_MOTION:
            "Waiting for Motion",
            linuxcnc.EXEC_WAITING_FOR_MOTION_QUEUE:
            "Waiting for Motion Queue",
            linuxcnc.EXEC_WAITING_FOR_IO:
            "Waiting for Pause",
            linuxcnc.EXEC_WAITING_FOR_MOTION_AND_IO:
            "Waiting for Motion and IO",
            linuxcnc.EXEC_WAITING_FOR_DELAY:
            "Waiting for Delay",
            linuxcnc.EXEC_WAITING_FOR_SYSTEM_CMD:
            "Waiting for system CMD",
            linuxcnc.EXEC_WAITING_FOR_SPINDLE_ORIENTED:
            "Waiting for spindle orient"
        }

        return exec_states[STAT.exec_state]

    @DataChannel
    def interp_state(self, chan):
        """Current state of RS274NGC interpreter

        1) Idle
        2) Reading
        3) Paused
        4) Waiting

        To return the string in a status label::

            status:interp_state?string

        :returns: RS274 interpreter state
        :rtype: int, str
        """
        return STAT.interp_state

    @interp_state.tostring
    def interp_state(self, chan):
        interp_states = {
            0: "N/A",
            linuxcnc.INTERP_IDLE: "Idle",
            linuxcnc.INTERP_READING: "Reading",
            linuxcnc.INTERP_PAUSED: "Paused",
            linuxcnc.INTERP_WAITING: "Waiting"
        }

        return interp_states[STAT.interp_state]

    @DataChannel
    def interpreter_errcode(self, chan):
        """Current RS274NGC interpreter return code

        0) Ok
        1) Exit
        2) Finished
        3) Endfile
        4) File not open
        5) Error

        To return the string in a status label::

            status:interpreter_errcode?string

        :returns: interp error code
        :rtype: int, str
        """
        return STAT.interpreter_errcode

    @interpreter_errcode.tostring
    def interpreter_errcode(self, chan):
        interpreter_errcodes = {
            0: "Ok",
            1: "Exit",
            2: "Finished",
            3: "Endfile",
            4: "File not open",
            5: "Error"
        }

        return interpreter_errcodes[STAT.interpreter_errcode]

    @DataChannel
    def task_state(self, chan, query=None):
        """Current status of task

        1) E-Stop
        2) Reset
        3) Off
        4) On

        To return the string in a status label::

            status:task_state?string

        :returns: current task state
        :rtype: int, str
        """
        return STAT.task_state

    @task_state.tostring
    def task_state(self, chan):
        task_states = {
            0: "N/A",
            linuxcnc.STATE_ESTOP: "E-Stop",
            linuxcnc.STATE_ESTOP_RESET: "Reset",
            linuxcnc.STATE_ON: "On",
            linuxcnc.STATE_OFF: "Off"
        }

        return task_states[STAT.task_state]

    @DataChannel
    def task_mode(self, chan):
        """Current task mode

        1) Manual
        2) Auto
        3) MDI

        To return the string in a status label::

            status:task_mode?string

        :returns: current task mode
        :rtype: int, str
        """
        return STAT.task_mode

    @task_mode.tostring
    def task_mode(self, chan):
        task_modes = {
            0: "N/A",
            linuxcnc.MODE_MANUAL: "Manual",
            linuxcnc.MODE_AUTO: "Auto",
            linuxcnc.MODE_MDI: "MDI"
        }

        return task_modes[STAT.task_mode]

    @DataChannel
    def motion_mode(self, chan):
        """Current motion controller mode

        1) Free
        2) Coord
        3) Teleop

        To return the string in a status label::

            status:motion_mode?string

        :returns: current motion mode
        :rtype: int, str
        """
        return STAT.motion_mode

    @motion_mode.tostring
    def motion_mode(self, chan):
        modes = {
            0: "N/A",
            linuxcnc.TRAJ_MODE_COORD: "Coord",
            linuxcnc.TRAJ_MODE_FREE: "Free",
            linuxcnc.TRAJ_MODE_TELEOP: "Teleop"
        }

        return modes[STAT.motion_mode]

    @DataChannel
    def motion_type(self, chan, query=None):
        """Motion type

        0) None
        1) Traverse
        2) Linear Feed
        3) Arc Feed
        4) Tool Change
        5) Probing
        6) Rotary Index

        To return the string in a status label::

            status:motion_type?string

        :returns:  current motion type
        :rtype: int, str
        """
        return STAT.motion_type

    @motion_type.tostring
    def motion_type(self, chan):
        motion_types = {
            0: "None",
            linuxcnc.MOTION_TYPE_TRAVERSE: "Traverse",
            linuxcnc.MOTION_TYPE_FEED: "Linear Feed",
            linuxcnc.MOTION_TYPE_ARC: "Arc Feed",
            linuxcnc.MOTION_TYPE_TOOLCHANGE: "Tool Change",
            linuxcnc.MOTION_TYPE_PROBING: "Probing",
            linuxcnc.MOTION_TYPE_INDEXROTARY: "Rotary Index"
        }

        return motion_types[STAT.motion_type]

    @DataChannel
    def program_units(self, chan):
        """Program units

        Available as an integer, or in short or long string formats.

        1) in, Inches
        2) mm, Millimeters
        3) cm, Centimeters

        To return the string in a status label::

            status:program_units
            status:program_units?string
            status:program_units?string&format=long

        :returns: current program units
        :rtype: int, str
        """
        return STAT.program_units

    @program_units.tostring
    def program_units(self, chan, format='short'):
        if format == 'short':
            return ["N/A", "in", "mm", "cm"][STAT.program_units]
        else:
            return ["N/A", "Inches", "Millimeters",
                    "Centimeters"][STAT.program_units]

    @DataChannel
    def linear_units(self, chan):
        """Machine linear units

        Available as float (units/mm), or in short or long string formats.

        To return the string in a status label::

            status:linear_units
            status:linear_units?string
            status:linear_units?string&format=long

        :returns: machine linear units
        :rtype: float, str
        """
        return STAT.linear_units

    @linear_units.tostring
    def linear_units(self, chan, format='short'):
        if format == 'short':
            return {0.0: "N/A", 1.0: "mm", 1 / 25.4: "in"}[STAT.linear_units]
        else:
            return {
                0.0: "N/A",
                1.0: "Millimeters",
                1 / 25.4: "Inches"
            }[STAT.linear_units]

    @DataChannel
    def gcodes(self, chan, fmt=None):
        """G-codes

        active G-codes for each modal group

        | syntax ``status:gcodes`` returns tuple of strings
        | syntax ``status:gcodes?raw`` returns tuple of integers
        | syntax ``status:gcodes?string`` returns str
        """
        if fmt == 'raw':
            return STAT.gcodes
        return chan.value

    @gcodes.tostring
    def gcodes(self, chan):
        return " ".join(chan.value)

    @gcodes.setter
    def gcodes(self, chan, gcodes):
        chan.value = tuple(
            ["G%g" % (c / 10.) for c in sorted(gcodes[1:]) if c != -1])
        chan.signal.emit(self.gcodes.value)

    @DataChannel
    def mcodes(self, chan, fmt=None):
        """M-codes

        active M-codes for each modal group

        | syntax ``status:mcodes`` returns tuple of strings
        | syntax ``status:mcodes?raw`` returns tuple of integers
        | syntax ``status:mcodes?string`` returns str
        """
        if fmt == 'raw':
            return STAT.mcodes
        return chan.value

    @mcodes.tostring
    def mcodes(self, chan):
        return " ".join(chan.value)

    @mcodes.setter
    def mcodes(self, chan, gcodes):
        chan.value = tuple(
            ["M%g" % gcode for gcode in sorted(gcodes[1:]) if gcode != -1])
        chan.signal.emit(chan.value)

    @DataChannel
    def g5x_index(self, chan):
        """Current G5x work coord system

        | syntax ``status:g5x_index`` returns int
        | syntax ``status:g5x_index?string`` returns str
        """
        return STAT.g5x_index

    @g5x_index.tostring
    def g5x_index(self, chan):
        return [
            "G53", "G54", "G55", "G56", "G57", "G58", "G59", "G59.1", "G59.2",
            "G59.3"
        ][STAT.g5x_index]

    @DataChannel
    def settings(self, chan, item=None):
        """Interpreter Settings

        Available Items:
            0) sequence_number
            1) feed
            2) speed

        :return: interpreter settings
        :rtype: tuple, int, float
        """
        if item is None:
            return STAT.settings
        return STAT.settings[{
            'sequence_number': 0,
            'feed': 1,
            'speed': 2
        }[item]]

    @DataChannel
    def homed(self, chan, anum=None):
        """Axis homed status

        If no axis number is specified returns a tuple of integers.
        If ``anum`` is specified returns True if the axis is homed, else False.

        Rules syntax::

            status:homed
            status:homed?anum=0

        Args:
         anum (int, optional) : the axis number to return the homed state of.

        :returns: axis homed states
        :rtype: tuple, bool

        """
        if anum is None:
            return STAT.homed
        return bool(STAT.homed[int(anum)])

    @DataChannel
    def all_axes_homed(self, chan):
        """All axes homed status

        True if all axes are homed or if [TRAJ]NO_FORCE_HOMING set in INI.

        :returns: all homed
        :rtype: bool
        """
        return chan.value

    @all_axes_homed.setter
    def all_axes_homed(self, chan, homed):
        if self.no_force_homing:
            chan.value = True
        else:
            for anum in INFO.AXIS_NUMBER_LIST:
                if homed[anum] is not 1:
                    chan.value = False
                    break
            else:
                chan.value = True
        chan.signal.emit(chan.value)

    # this is used by File "qtpyvcp/qtpyvcp/actions/program_actions.py",
    # line 83, in _run_ok elif not STATUS.allHomed():

    def allHomed(self):
        if self.no_force_homing:
            return True
        for jnum in range(STAT.joints):
            if not STAT.joint[jnum]['homed']:
                return False
        return True

    def initialise(self):
        """Start the periodic update timer."""

        # watch the gcode file for changes and reload as needed
        self.file_watcher = QFileSystemWatcher()
        self.file_watcher.addPath(self.file.value)
        self.file_watcher.fileChanged.connect(self.updateFile)

        LOG.debug("Starting periodic updates with %ims cycle time",
                  self._cycle_time)
        self.timer.start(self._cycle_time)

    def terminate(self):
        """Save persistent settings on terminate."""
        PREFS.setPref("STATUS", "RECENT_FILES", self.recent_files.value)
        PREFS.setPref("STATUS", "MAX_RECENT_FILES", self.max_recent_files)

    def _periodic(self):

        # s = time.time()

        try:
            STAT.poll()
        except Exception:
            LOG.warning("Status polling failed, is LinuxCNC running?",
                        exc_info=True)
            self.timer.stop()
            return

        # status updates
        for item, old_val in self.old.iteritems():
            new_val = getattr(STAT, item)
            if new_val != old_val:
                self.old[item] = new_val
                self.channels[item].setValue(new_val)

        # joint status updates
        for joint in self.joint:
            joint._update()

        # spindle status updates
        for spindle in self.spindle:
            spindle._update()
Example #10
0
class OffsetTable(DataPlugin):
    DEFAULT_OFFSET = {
        0: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        1: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        2: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        3: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        4: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        5: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        6: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        7: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
        8: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
    }

    NO_TOOL = merge(DEFAULT_OFFSET, {'T': 0, 'R': 'No Tool Loaded'})  # FIXME Requires safe removal

    COLUMN_LABELS = [
        'X',
        'Y',
        'Z',
        'A',
        'B',
        'C',
        'U',
        'V',
        'W',
        'R'
    ]

    ROW_LABELS = [
        'G54',
        'G55',
        'G56',
        'G57',
        'G58',
        'G59',
        'G59.1',
        'G59.2',
        'G59.3'
    ]

    offset_table_changed = Signal(dict)
    active_offset_changed = Signal(int)

    def __init__(self, columns='XYZABCUVWR', file_header_template=None):
        super(OffsetTable, self).__init__()

        file_name = INFO.getParameterFile()

        self.parameter_file = None
        if file_name:
            self.parameter_file = os.path.join(os.path.dirname(os.path.realpath(file_name)), file_name)

        self.fs_watcher = None

        self.command = linuxcnc.command()
        self.status = STATUS

        self.columns = self.validateColumns(columns) or [c for c in 'XYZABCUVWR']
        self.rows = self.ROW_LABELS

        self.setCurrentOffsetNumber(1)

        self.g5x_offset_table = self.DEFAULT_OFFSET.copy()
        self.current_index = STATUS.stat.g5x_index

        self.loadOffsetTable()

        self.status.g5x_index.notify(self.setCurrentOffsetNumber)

    @DataChannel
    def current_offset(self, chan, item=None):
        """Current Offset Info

        Available items:

        * X -- x offset
        * Y -- y offset
        * Z -- z offset
        * A -- a offset
        * B -- b offset
        * C -- c offset
        * U -- u offset
        * V -- v offset
        * W -- w offset
        * R -- r offset

        Rules channel syntax::

            offsettable:current_offset
            offsettable:current_offset?X
            offsettable:current_offset?x_offset

        :param item: the name of the tool data item to get
        :return: dict, int, float, str
        """
        return self.current_offset

    def initialise(self):
        self.fs_watcher = QFileSystemWatcher([self.parameter_file])
        self.fs_watcher.fileChanged.connect(self.onParamsFileChanged)

    @staticmethod
    def validateColumns(columns):
        """Validate display column specification.

        The user can specify columns in multiple ways, method is used to make
        sure that that data is validated and converted to a consistent format.

        Args:
            columns (str | list) : A string or list of the column IDs
                that should be shown in the tooltable.

        Returns:
            None if not valid, else a list of uppercase column IDs.
        """
        if not isinstance(columns, (basestring, list, tuple)):
            return

        return [col for col in [col.strip().upper() for col in columns]
                if col in 'XYZABCUVWR' and not col == '']

    # def newOffset(self, tnum=None):
    #     """Get a dict of default tool values for a new tool."""
    #     if tnum is None:
    #         tnum = len(self.OFFSET_TABLE)
    #     new_tool = self.DEFAULT_OFFSET.copy()
    #     new_tool.update({'T': tnum, 'P': tnum, 'R': 'New Tool'})
    #     return new_tool

    def onParamsFileChanged(self, path):
        LOG.debug('Params file changed: {}'.format(path))
        # ToolEdit deletes the file and then rewrites it, so wait
        # a bit to ensure the new data has been writen out.
        QTimer.singleShot(50, self.reloadOffsetTable)

    def setCurrentOffsetNumber(self, offset_num):
        self.current_offset.setValue(offset_num)
        self.current_index = offset_num
        self.active_offset_changed.emit(offset_num)

    def reloadOffsetTable(self):
        # rewatch the file if it stop being watched because it was deleted
        if self.parameter_file not in self.fs_watcher.files():
            self.fs_watcher.addPath(self.parameter_file)

        # reload with the new data
        offset_table = self.loadOffsetTable()

    def iterTools(self, offset_table=None, columns=None):
        offset_table = offset_table or self.OFFSET_TABLE
        columns = self.validateColumns(columns) or self.columns
        for offset in sorted(offset_table.iterkeys()):
            offset_data = offset_table[offset]
            yield [offset_data[key] for key in columns]

    def loadOffsetTable(self):

        if self.parameter_file:
            with open(self.parameter_file, 'r') as fh:
                for line in fh:
                    param, data = int(line.split()[0]), float(line.split()[1])

                    if 5230 >= param >= 5221:
                        self.g5x_offset_table.get(0)[param - 5221] = data
                    elif 5250 >= param >= 5241:
                        self.g5x_offset_table.get(1)[param - 5241] = data
                    elif 5270 >= param >= 5261:
                        self.g5x_offset_table.get(2)[param - 5261] = data
                    elif 5290 >= param >= 5281:
                        self.g5x_offset_table.get(3)[param - 5281] = data
                    elif 5310 >= param >= 5301:
                        self.g5x_offset_table.get(4)[param - 5301] = data
                    elif 5330 >= param >= 5321:
                        self.g5x_offset_table.get(5)[param - 5321] = data
                    elif 5350 >= param >= 5341:
                        self.g5x_offset_table.get(6)[param - 5341] = data
                    elif 5370 >= param >= 5361:
                        self.g5x_offset_table.get(7)[param - 5361] = data
                    elif 5390 >= param >= 5381:
                        self.g5x_offset_table.get(8)[param - 5381] = data

        self.offset_table_changed.emit(self.g5x_offset_table)

        return self.g5x_offset_table

    def getOffsetTable(self):
        return self.g5x_offset_table

    def saveOffsetTable(self, offset_table, columns):
        """ Stores the offset table in memory.

        Args:
            offset_table (dict) : Dictionary of dictionaries containing
                the tool data to write to the file.
            columns (str | list) : A list of data columns to write.
                If `None` will use the value of ``self.columns``.
        """
        self.g5x_offset_table = offset_table

        for index in range(len(self.rows)):
            mdi_list = list()
            mdi_list.append("G10 L2")
            mdi_list.append("P{}".format(index+1))

            for char in columns:

                column_index = self.columns.index(char)

                mdi_list.append("{}{}".format(char, self.g5x_offset_table[index][column_index]))

            mdi_command = " ".join(mdi_list)

            issue_mdi(mdi_command)
Example #11
0
 def initialise(self):
     self.fs_watcher = QFileSystemWatcher([self.parameter_file])
     self.fs_watcher.fileChanged.connect(self.onParamsFileChanged)
Example #12
0
class FolderBrowser(FileSystemTreeView):
    def __init__(self, parent, ide, path, dock_widget):

        super(FolderBrowser, self).__init__(parent)
        self.main_window = parent
        self._path = path
        self._ide = ide
        self._dock_widget = dock_widget
        self.clear_ignore_patterns()
        self.add_ignore_patterns(ide.ignore_patterns)
        self.set_root_path(os.path.normpath(path))
        self.set_context_menu(FileSystemContextMenu())
        self._watcher = QFileSystemWatcher()
        self._watcher.addPath(path)
        self._watcher.fileChanged.connect(self._index_files)
        self._watcher.directoryChanged.connect(self._index_files)
        self._indexing = False
        self._file_list = []
        self._index_files()

    def currentChanged(self, current_index, previous_index):

        # If the current item has changed, then the container dock widget
        # should be raised. This is necessary for locating files in dock
        # widgets that are tabbed and not active.
        super(FolderBrowser, self).currentChanged(current_index,
                                                  previous_index)
        self._dock_widget.raise_()

    def mouseDoubleClickEvent(self, event):

        path = self.fileInfo(self.indexAt(event.pos())).filePath()
        self.open_file(path)

    def open_file(self, path):

        if not os.path.exists(path) or os.path.isdir(path):
            return
        self._ide.open_document(path)

    @property
    def file_list(self):

        while self._indexing:
            oslogger.debug(u'still indexing {}'.format(self._path))
            time.sleep(.1)
            QApplication.processEvents()
        return self._file_list

    def _index_files(self, _=None):

        if self._indexing:
            oslogger.debug(u'indexing in progress for {}'.format(self._path))
            return
        self._indexing = True
        self._queue = multiprocessing.Queue()
        self._file_indexer = multiprocessing.Process(
            target=file_indexer,
            args=(self._queue, self._path, self._ide.ignore_patterns,
                  cfg.opensesame_ide_max_index_files))
        self._file_indexer.start()
        oslogger.debug(u'indexing {} (PID={})'.format(self._path,
                                                      self._file_indexer.pid))
        QTimer.singleShot(1000, self._check_file_indexer)

    def _check_file_indexer(self):

        if self._queue.empty():
            oslogger.debug(u'queue still empty for {}'.format(self._path))
            QTimer.singleShot(1000, self._check_file_indexer)
            return
        self._file_list = self._queue.get()
        self._indexing = False
        if self._file_list is None:
            self._ide.extension_manager.fire(
                u'notify',
                message=_(u'Not indexing {} (too many files)').format(
                    self._path),
                category=u'warning')
            self._file_list = []
            return
        oslogger.debug(u'{} files indexed for {}'.format(
            len(self._file_list), self._path))
        self._file_indexer.join()
        try:
            self._file_indexer.close()
        except AttributeError:
            # Process.close() was introduced only in Python 3.7
            pass
        QTimer.singleShot(300000, self._index_files)
Example #13
0
class FolderBrowser(FileSystemTreeView):
    def __init__(self, parent, ide, path, dock_widget):

        super(FolderBrowser, self).__init__(parent)
        self.main_window = parent
        self._path = path
        self._ide = ide
        self._dock_widget = dock_widget
        self._dock_widget._gitignore_checkbox.stateChanged.connect(
            self._toggle_gitignore)
        self._set_ignore_patterns()
        self.set_root_path(os.path.normpath(path))
        self.set_context_menu(FileSystemContextMenu(self.main_window))
        self._watcher = QFileSystemWatcher()
        self._watcher.addPath(path)
        self._watcher.fileChanged.connect(self._on_file_changed)
        self._watcher.directoryChanged.connect(self._on_folder_changed)
        self._indexing = False
        self._file_list = []
        self._active = True
        self._timer = None
        self._index_files()

    def _refresh(self):
        """Refreshes the file tree"""
        self.set_root_path(self.root_path)

    def _toggle_gitignore(self):
        """Is called when the .gitignore option is (un)checked. Updates the
        ignore patterns and refreshes the file tree.
        """
        self._set_ignore_patterns()
        self._restart_indexing()
        self._refresh()

    def _set_ignore_patterns(self):
        """Sets the ignore patterns based on the configuration as well as the
        .gitignore if available and activated.
        """
        self.clear_ignore_patterns()
        self.add_ignore_patterns(self._ide.ignore_patterns)
        gitignore = os.path.join(self._path, u'.gitignore')
        if not os.path.exists(gitignore):
            self._dock_widget._gitignore_checkbox.hide()
            return
        self._dock_widget._gitignore_checkbox.show()
        if self._dock_widget._gitignore_checkbox.isChecked():
            with open(gitignore) as fd:
                self.add_ignore_patterns(fd.read().splitlines())

    def currentChanged(self, current_index, previous_index):
        """If the current item has changed, then the container dock widget
        should be raised. This is necessary for locating files in dock widgets
        that are tabbed and not active.
        """
        super(FolderBrowser, self).currentChanged(current_index,
                                                  previous_index)
        self._dock_widget.raise_()

    def mouseDoubleClickEvent(self, event):
        """Opens a file when it's double-clicked"""
        path = self.fileInfo(self.indexAt(event.pos())).filePath()
        self.open_file(path)

    def shutdown(self):

        self._active = False
        self._watcher.fileChanged.disconnect()
        self._watcher.directoryChanged.disconnect()
        self._stop_indexing()

    def open_file(self, path):

        if not os.path.exists(path) or os.path.isdir(path):
            return
        self._ide.open_document(path)

    @property
    def file_list(self):

        while self._indexing:
            oslogger.debug(u'still indexing {}'.format(self._path))
            time.sleep(.1)
            QApplication.processEvents()
        return self._file_list

    def _on_file_changed(self, _=None):

        oslogger.debug(u'file changed in {}'.format(self._path))
        self._restart_indexing()

    def _on_folder_changed(self, _=None):

        oslogger.debug(u'folder changed in {}'.format(self._path))
        self._restart_indexing()

    def _stop_indexing(self):
        """Stops the file indexer by stopping any active timers and by
        killing running indexer processes.
        """
        if self._timer is not None and self._timer.isActive():
            oslogger.debug(u'stopping timer for {}'.format(self._path))
            self._timer.stop()
        if self._indexing:
            oslogger.debug(u'killing indexer for {}'.format(self._path))
            self._file_indexer.terminate()
            self._indexing = False

    def _restart_indexing(self):
        """Restarts the file indexer"""
        self._stop_indexing()
        self._index_files()

    def _start_timer(self, msec, target):
        self._timer = QTimer()
        self._timer.setSingleShot(True)
        self._timer.setInterval(msec)
        self._timer.timeout.connect(target)
        self._timer.start()

    def _index_files(self, _=None):

        if not self._active:
            oslogger.debug(u'shutting down indexing for {}'.format(self._path))
            return
        if self._indexing:
            oslogger.debug(u'indexing in progress for {}'.format(self._path))
            return
        self._indexing = True
        self._queue = multiprocessing.Queue()
        self._file_indexer = multiprocessing.Process(
            target=file_indexer,
            args=(self._queue, self._path, self._ignored_patterns,
                  cfg.opensesame_ide_max_index_files))
        self._file_indexer.start()
        oslogger.debug(u'indexing {} (PID={})'.format(self._path,
                                                      self._file_indexer.pid))
        self._ide.extension_manager.fire('register_subprocess',
                                         pid=self._file_indexer.pid,
                                         description='file_indexer:{}'.format(
                                             self._path))
        self._start_timer(1000, self._check_file_indexer)

    def _check_file_indexer(self):

        if self._queue.empty():
            oslogger.debug(u'queue still empty for {}'.format(self._path))
            self._start_timer(1000, self._check_file_indexer)
            return
        self._file_list = self._queue.get()
        self._indexing = False
        if self._file_list is None:
            self._ide.extension_manager.fire(
                u'notify',
                message=_(u'Not indexing {} (too many files)').format(
                    self._path),
                category=u'info')
            self._file_list = []
            return
        oslogger.debug(u'{} files indexed for {}'.format(
            len(self._file_list), self._path))
        self._file_indexer.join()
        try:
            self._file_indexer.close()
        except AttributeError:
            # Process.close() was introduced only in Python 3.7
            pass
        self._start_timer(300000, self._index_files)
    def __init__(self):
        super(ProjectExplorer, self).__init__()

        self.setWindowTitle('ProjectExplorer')

        # --- Setup the tab bar ---
        self._tab_bar = ExtendedTabBar()

        self._tab_bar.setShape(ExtendedTabBar.RoundedEast)
        self._tab_bar.setTabsClosable(True)
        self._tab_bar.setMovable(True)
        self._tab_bar.setUsesScrollButtons(True)
        self._tab_bar.setDrawBase(False)

        open_project_action = QAction('Open Project', self)
        self._tab_bar.floating_toolbar.addAction(open_project_action)
        (
            self
            ._tab_bar
            .floating_toolbar
            .widgetForAction(open_project_action)
            .setObjectName('open_project')
        )
        open_project_action.triggered.connect(self._open_project)

        new_project_action = QAction('New Project', self)
        self._tab_bar.floating_toolbar.addAction(new_project_action)
        (
            self
            ._tab_bar
            .floating_toolbar
            .widgetForAction(new_project_action)
            .setObjectName('new_project')
        )
        new_project_action.triggered.connect(self._new_project)

        settings_action = QAction('Settings', self)
        self._tab_bar.right_toolbar.addAction(settings_action)
        self._tab_bar.right_toolbar.widgetForAction(settings_action).setObjectName('settings')
        settings_action.triggered.connect(self._open_settings)

        # --- Setup the tab widget ---
        self._tab_widget = ExtendedTabWidget()
        self._tab_widget.setTabBar(self._tab_bar)

        main_layout = QHBoxLayout()
        main_layout.setSpacing(0)
        main_layout.setContentsMargins(0, 0, 0, 0)

        main_layout.addWidget(self._tab_widget)
        main_layout.addWidget(self._tab_bar)
        self.setLayout(main_layout)

        self._settings_watcher = QFileSystemWatcher()
        self._settings_watcher.fileChanged.connect(self.load_settings)

        self._settings = None
        self._load_settings()

        # There needs to be a delay after the settings watcher triggers to allow external
        # applications to finish writing to disk.
        self._settings_load_delay = QTimer()
        self._settings_load_delay.setSingleShot(True)
        self._settings_load_delay.setInterval(200)
        self._settings_load_delay.timeout.connect(self._load_settings)

        self._new_project()
class ProjectExplorer(QFrame):
    '''
    A project explorer with tabs.

    Tab contents show different projects as different project tabs are selected.
    '''
    def __init__(self):
        super(ProjectExplorer, self).__init__()

        self.setWindowTitle('ProjectExplorer')

        # --- Setup the tab bar ---
        self._tab_bar = ExtendedTabBar()

        self._tab_bar.setShape(ExtendedTabBar.RoundedEast)
        self._tab_bar.setTabsClosable(True)
        self._tab_bar.setMovable(True)
        self._tab_bar.setUsesScrollButtons(True)
        self._tab_bar.setDrawBase(False)

        open_project_action = QAction('Open Project', self)
        self._tab_bar.floating_toolbar.addAction(open_project_action)
        (
            self
            ._tab_bar
            .floating_toolbar
            .widgetForAction(open_project_action)
            .setObjectName('open_project')
        )
        open_project_action.triggered.connect(self._open_project)

        new_project_action = QAction('New Project', self)
        self._tab_bar.floating_toolbar.addAction(new_project_action)
        (
            self
            ._tab_bar
            .floating_toolbar
            .widgetForAction(new_project_action)
            .setObjectName('new_project')
        )
        new_project_action.triggered.connect(self._new_project)

        settings_action = QAction('Settings', self)
        self._tab_bar.right_toolbar.addAction(settings_action)
        self._tab_bar.right_toolbar.widgetForAction(settings_action).setObjectName('settings')
        settings_action.triggered.connect(self._open_settings)

        # --- Setup the tab widget ---
        self._tab_widget = ExtendedTabWidget()
        self._tab_widget.setTabBar(self._tab_bar)

        main_layout = QHBoxLayout()
        main_layout.setSpacing(0)
        main_layout.setContentsMargins(0, 0, 0, 0)

        main_layout.addWidget(self._tab_widget)
        main_layout.addWidget(self._tab_bar)
        self.setLayout(main_layout)

        self._settings_watcher = QFileSystemWatcher()
        self._settings_watcher.fileChanged.connect(self.load_settings)

        self._settings = None
        self._load_settings()

        # There needs to be a delay after the settings watcher triggers to allow external
        # applications to finish writing to disk.
        self._settings_load_delay = QTimer()
        self._settings_load_delay.setSingleShot(True)
        self._settings_load_delay.setInterval(200)
        self._settings_load_delay.timeout.connect(self._load_settings)

        self._new_project()

    def _new_project(self):
        '''
        Creates a new project.
        '''
        # Get a unique project name.
        open_projects = {self._tab_bar.tabText(index) for index in range(self._tab_bar.count())}
        project_name_format = 'project_{}'
        project_count = 0
        while True:
            project_name = project_name_format.format(project_count)

            if project_name not in open_projects:
                break

            project_count += 1

        project = Project(project_name, self._settings)
        project.name_changed.connect(self._handle_name_change)
        project.add_root()

        index = self._tab_widget.addTab(project, project_name)
        self._tab_bar.setCurrentIndex(index)

    def _open_project(self):
        '''
        Opens a saved project.
        '''
        projects_directory = self._settings['projects_directory']

        if not os.path.isdir(projects_directory):
            os.makedirs(projects_directory)

        path, filter_ = QFileDialog.getOpenFileName(
            self, 'Open Project', os.path.join(projects_directory))

        if path == '' and filter_ == '':
            return

        project = Project.open(path, self._settings)
        project.name_changed.connect(self._handle_name_change)
        index = self._tab_widget.addTab(project, project.name)
        self._tab_bar.setCurrentIndex(index)

    def _handle_name_change(self):
        '''
        Updates the tab text of projects that have been saved.
        '''
        project = self.sender()
        sender_index = self._tab_widget.indexOf(project)
        self._tab_bar.setTabText(sender_index, project.name)

    def _open_settings(self):
        '''
        Opens the settings file.
        '''
        os.startfile(SETTINGS_PATH)

    def _apply_theme_settings(self):
        '''
        Apply the theme specified in the settings.
        '''
        theme_path = self._settings['theme']
        try:
            with open(theme_path) as theme:
                style_sheet = theme.read()
        except IOError:
            QMessageBox.critical(
                self,
                'Invalid Setting',
                'Unable to load theme:\n"{}"'.format(theme_path))

            return

        style_sheet = scss.Compiler().compile_string(style_sheet)

        self.setStyleSheet(style_sheet)

    def _load_settings(self):
        '''
        Since the settings file is modified by an external program, load_settings() must wait some
        delay to allow the file to be completely written. This function does the actually settings
        load.
        '''
        # Make sure the settings exist.
        if not os.path.isfile(SETTINGS_PATH):
            shutil.copy(DEFAULT_SETTINGS_PATH, SETTINGS_PATH)

        # Make sure the settings are being watched. Note that this needs to be outside of the
        # existence check above, otherwise the settings won't be watched if they already exist.
        if len(self._settings_watcher.files()) == 0:
            self._settings_watcher.addPath(SETTINGS_PATH)

        try:
            self._settings = extended_json.load_file(SETTINGS_PATH)
        except extended_json.JSONSyntaxError as error:
            message_box = QMessageBox(
                QMessageBox.Critical,
                'Error',
                f'{error.msg}\n{error.context()}'
            )
            message_box.setFont(QFont('Consolas'))
            message_box.exec()

            if self._settings is None:
                sys.exit()

        self._apply_theme_settings()

        # Update the setting of all the open project widgets.
        projects = (self._tab_widget.widget(index) for index in range(self._tab_widget.count()))
        for project in projects:
            project.update_settings(self._settings)

    def load_settings(self):
        '''
        Loads the settings file.
        '''
        self._settings_load_delay.start()
Example #16
0
class VCPApplication(QApplication):
    def __init__(self, theme=None, stylesheet=None, custom_fonts=[]):
        app_args = (qtpyvcp.OPTIONS.command_line_args or "").split()
        super(VCPApplication, self).__init__(app_args)

        opts = qtpyvcp.OPTIONS

        self.status = getPlugin('status')

        # initialize plugins
        initialisePlugins()

        theme = opts.theme or theme
        if theme is not None:
            self.setStyle(QStyleFactory.create(theme))

        stylesheet = opts.stylesheet or stylesheet
        if stylesheet is not None:
            self.loadStylesheet(stylesheet, opts.develop)

        if custom_fonts:
            if isinstance(custom_fonts, str):  # single font or location
                self.loadCustomFont(custom_fonts)
            else:  # list of fonts or locations
                for font in custom_fonts:
                    self.loadCustomFont(font)

        # self.window = self.loadVCPMainWindow(opts, vcp_file)
        # if self.window is not None:
        #     self.window.show()

        if opts.hide_cursor:
            from qtpy.QtGui import QCursor
            self.setOverrideCursor(QCursor(Qt.BlankCursor))

        # Performance monitoring
        if opts.perfmon:
            import psutil
            self.perf = psutil.Process()
            self.perf_timer = QTimer()
            self.perf_timer.setInterval(2000)
            self.perf_timer.timeout.connect(self.logPerformance)
            self.perf_timer.start()

        self.aboutToQuit.connect(self.terminate)

    def loadVCPMainWindow(self, opts, vcp_file=None):
        """
        Loads a VCPMainWindow instance defined by a Qt .ui file, a Python .py
        file, or from a VCP python package.

        Parameters
        ----------
        vcp_file : str
            The path or name of the VCP to load.
        opts : OptDict
            A OptDict of options to pass to the VCPMainWindow subclass.

        Returns
        -------
        VCPMainWindow instance
        """
        vcp = opts.vcp or vcp_file
        if vcp is None:
            return

        if os.path.exists(vcp):

            vcp_path = os.path.realpath(vcp)
            if os.path.isfile(vcp_path):
                directory, filename = os.path.split(vcp_path)
                name, ext = os.path.splitext(filename)
                if ext == '.ui':
                    LOG.info(
                        "Loading VCP from UI file: yellow<{}>".format(vcp))
                    return VCPMainWindow(opts=opts, ui_file=vcp_path)
                elif ext == '.py':
                    LOG.info(
                        "Loading VCP from PY file: yellow<{}>".format(vcp))
                    return self.loadPyFile(vcp_path, opts)
            elif os.path.isdir(vcp_path):
                LOG.info("VCP is a directory")
                # TODO: Load from a directory if it has a __main__.py entry point
        else:
            try:
                entry_points = {}
                for entry_point in iter_entry_points(
                        group='qtpyvcp.example_vcp'):
                    entry_points[entry_point.name] = entry_point
                for entry_point in iter_entry_points(group='qtpyvcp.vcp'):
                    entry_points[entry_point.name] = entry_point
                window = entry_points[vcp.lower()].load()
                return window(opts=opts)
            except:
                LOG.exception("Failed to load entry point")

        LOG.critical("VCP could not be loaded: yellow<{}>".format(vcp))
        sys.exit()

    def loadPyFile(self, pyfile, opts):
        """
        Load a .py file, performs some sanity checks to try and determine
        if the file actually contains a valid VCPMainWindow subclass, and if
        the checks pass, create and return an instance.

        This is an internal method, users will usually want to use `loadVCP` instead.

        Parameters
        ----------
        pyfile : str
            The path to a .py file to load.
        opts : OptDict
            A OptDict of options to pass to the VCPMainWindow subclass.

        Returns
        -------
        VCPMainWindow instance
        """
        # Add the pyfile module directory to the python path, so that submodules can be loaded
        module_dir = os.path.dirname(os.path.abspath(pyfile))
        sys.path.append(module_dir)

        # Load the module. It's attributes can be accessed via `python_vcp.attr`
        module = imp.load_source('python_vcp', pyfile)

        classes = [
            obj for name, obj in inspect.getmembers(module)
            if inspect.isclass(obj) and issubclass(obj, VCPMainWindow)
            and obj != VCPMainWindow
        ]
        if len(classes) == 0:
            raise ValueError(
                "Invalid File Format."
                " {} has no class inheriting from VCPMainWindow.".format(
                    pyfile))
        if len(classes) > 1:
            LOG.warn(
                "More than one VCPMainWindow class in file yellow<{}>."
                " The first occurrence (in alphabetical order) will be used: {}"
                .format(pyfile, classes[0].__name__))
        cls = classes[0]

        # initialize and return the VCPMainWindow subclass
        return cls(opts=opts)

    def loadStylesheet(self, stylesheet, watch=False):
        """Loads a QSS stylesheet file containing styles to be applied
        to specific Qt and/or QtPyVCP widget classes.

        Args:
            stylesheet (str) : Path to the .qss stylesheet to load.
            watch (bool) : Whether to watch and re-load on .qss file changes.
        """
        def load(path):
            LOG.info(
                "Loading global stylesheet: yellow<{}>".format(stylesheet))
            self.setStyleSheet("file:///" + path)

            if watch:
                from qtpy.QtCore import QFileSystemWatcher
                self.qss_file_watcher = QFileSystemWatcher()
                self.qss_file_watcher.addPath(stylesheet)
                self.qss_file_watcher.fileChanged.connect(load)

        load(stylesheet)

    def loadCustomFont(self, font):
        """Loads custom front from a file or directory."""

        if os.path.isfile(font) and os.path.splitext(font)[1] in [
                '.ttf', '.otf', '.woff', '.woff2'
        ]:
            self.addApplicationFont(font)
        elif os.path.isdir(font):
            for ffile in os.listdir(font):
                fpath = os.path.join(font, ffile)
                self.loadCustomFont(fpath)

    def addApplicationFont(self, font_path):
        """Loads a font file into the font database. The path can specify the
        location of a font file or a qresource."""
        LOG.debug("Loading custom font: %s" % font_path)
        res = QFontDatabase.addApplicationFont(font_path)
        # per QT docs -1 is error and 0+ is index to font loaded for later use
        if res < 0:
            LOG.error("Failed to load font: %s", font_path)

    def getWidget(self, name):
        """Searches for a widget by name in the application windows.

        Args:
            name (str) : ObjectName of the widget.

        Returns: QWidget
        """
        for win_name, obj in list(qtpyvcp.WINDOWS.items()):
            if hasattr(obj, name):
                return getattr(obj, name)

        raise AttributeError("Could not find widget with name: %s" % name)

    @Slot()
    def logPerformance(self):
        """Logs total CPU usage (in percent), as well as per-thread usage.
        """
        with self.perf.oneshot():
            total_percent = self.perf.cpu_percent(interval=None)
            total_time = sum(self.perf.cpu_times())
            usage = [
                "{:.3f}".format(total_percent *
                                ((t.system_time + t.user_time) / total_time))
                for t in self.perf.threads()
            ]

        LOG.info("Performance:\n"
                 "    Total CPU usage (%): {}\n"
                 "    Per Thread: {}\n".format(total_percent, ' '.join(usage)))

    def terminate(self):
        self.terminateWidgets()
        terminatePlugins()

    def initialiseWidgets(self):
        for w in self.allWidgets():
            if isinstance(w, VCPPrimitiveWidget):
                w.initialize()

    def terminateWidgets(self):
        LOG.debug("Terminating widgets")
        for w in self.allWidgets():
            if isinstance(w, VCPPrimitiveWidget):
                try:
                    w.terminate()
                except Exception:
                    LOG.exception('Error terminating %s widget', w)