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)
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 __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 __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()
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()
def initialise(self): self.fs_watcher = QFileSystemWatcher() self.fs_watcher.addPath(self.tool_table_file) self.fs_watcher.fileChanged.connect(self.onToolTableFileChanged)
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()
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)
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()
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)
def initialise(self): self.fs_watcher = QFileSystemWatcher([self.parameter_file]) self.fs_watcher.fileChanged.connect(self.onParamsFileChanged)
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)
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()
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)