Esempio n. 1
0
class Trace(Module):
    '''
    Module to manage all of the different traces (with unique names/colors) and the
    Crosshairs objects associated to each one.  In particular, handles creation/modfication
    of traces and crosshairs.
    '''
    def __init__(self, app):
        info(' - initializing module: Trace')

        self.app = app

        # some images for the buttons
        # Source for icons: https://material.io/tools/icons/?style=outline
        # License: Apache Version 2.0 www.apache.org/licenses/LICENSE-2.0.txt
        data_copy = '''R0lGODlhGAAYAPAAAAAAAAAAACH5BAEAAAEALAAAAAAYABgAAAJHjI+pCe3/1oHUSdOunmDvHFTWBYrjUnbMuWIqAqEqCMdt+HI25yrVTZMEcT3NMPXJEZckJdKorCWbU2H0JqvKTBErl+XZFAAAOw'''
        data_paste = '''R0lGODlhGAAYAPAAAAAAAAAAACH5BAEAAAEALAAAAAAYABgAAAJBjI+pq+DAonlPToqza7rv9FlBeJCSOUJpd3EXm7piDKoi+nkqvnttPaMhUAzeiwJMapJDm8U44+kynCkmiM1qZwUAOw'''

        self.img_copy = PhotoImage(data=data_copy)
        self.img_paste = PhotoImage(data=data_paste)

        self.displayedColour = None
        #self.app.Data.getCurrentTraceColor()

        # array of trace names for this directory
        self.available = self.app.Data.getTopLevel('traces')
        self.available = [] if self.available == None else self.available

        # dictionary to hold trace -> [crosshairs] data
        self.crosshairs = {}

        # set of currently selected crosshairs
        self.selected = set()

        # set of copied crosshairs
        self.copied = []

        # declare & init trace string variable
        self.traceSV = StringVar()
        self.traceSV.set('')

        # frame for (most of) our widgets
        self.frame = Frame(self.app.LEFT)  #, pady=7, padx=7)
        self.frame.grid(row=4)

        # listbox to contain all of our traces
        lbframe = Frame(self.frame)
        self.scrollbar = Scrollbar(lbframe)
        self.listbox = Listbox(lbframe,
                               yscrollcommand=self.scrollbar.set,
                               width=12,
                               exportselection=False,
                               takefocus=0)
        self.scrollbar.config(command=self.listbox.yview)
        for trace in self.available:
            self.listbox.insert('end', trace)
        for i, item in enumerate(self.listbox.get(0, 'end')):
            # select our "default trace"
            if item == self.app.Data.getTopLevel('defaultTraceName'):
                self.listbox.selection_clear(0, 'end')
                self.listbox.select_set(i)

        # this module is responsible for so many widgets that we need a different
        # strategy for keeping track of everything that needs constistent grid() /
        # grid_remove() behavior
        self.TkWidgets = [
            self.getWidget(Header(self.frame, text="Choose a trace"),
                           row=5,
                           column=0,
                           columnspan=4),
            self.getWidget(lbframe, row=10, column=0, rowspan=50),
            self.getWidget(Button(self.frame,
                                  text='Set as default',
                                  command=self.setDefaultTraceName,
                                  takefocus=0),
                           row=10,
                           column=2,
                           columnspan=2),
            self.getWidget(Button(self.frame,
                                  text='Select all',
                                  command=self.selectAll,
                                  takefocus=0),
                           row=11,
                           column=2,
                           columnspan=2),
            self.getWidget(Button(self.frame,
                                  image=self.img_copy,
                                  command=self.copy,
                                  takefocus=0),
                           row=12,
                           column=2),  # FIXME: add tooltip for "Copy"
            self.getWidget(Button(self.frame,
                                  image=self.img_paste,
                                  command=self.paste,
                                  takefocus=0),
                           row=12,
                           column=3),  # FIXME: add tooltip for "Paste"
            self.getWidget(Entry(self.frame,
                                 width=8,
                                 textvariable=self.displayedColour),
                           row=13,
                           column=1,
                           columnspan=2,
                           sticky='w'),
            self.getWidget(Button(self.frame,
                                  text='Recolor',
                                  command=self.recolor,
                                  takefocus=0),
                           row=13,
                           column=3),
            self.getWidget(Button(self.frame,
                                  text='Clear',
                                  command=self.clear,
                                  takefocus=0),
                           row=15,
                           column=2,
                           columnspan=2),
            self.getWidget(Entry(self.frame,
                                 width=12,
                                 textvariable=self.traceSV),
                           row=100,
                           column=0,
                           sticky='w'),
            self.getWidget(Button(self.frame,
                                  text='New',
                                  command=self.newTrace,
                                  takefocus=0),
                           row=100,
                           column=2),
            self.getWidget(Button(self.frame,
                                  text='Rename',
                                  command=self.renameTrace,
                                  takefocus=0),
                           row=100,
                           column=3)
        ]

        # there's probably a better way to do this than indexing into self.TkWidgets
        self.TkWidgets[6]['widget'].bind(
            '<Return>', lambda ev: self.TkWidgets[0]['widget'].focus())
        self.TkWidgets[6]['widget'].bind(
            '<Escape>', lambda ev: self.TkWidgets[0]['widget'].focus())
        self.TkWidgets[9]['widget'].bind(
            '<Return>', lambda ev: self.TkWidgets[0]['widget'].focus())
        self.TkWidgets[9]['widget'].bind(
            '<Escape>', lambda ev: self.TkWidgets[0]['widget'].focus())

        if util.get_platform() == 'Linux':
            self.app.bind('<Control-r>', self.recolor)
            self.app.bind('<Control-c>', self.copy)
            self.app.bind('<Control-v>', self.paste)
        else:
            self.app.bind('<Command-r>', self.recolor)
            self.app.bind('<Command-c>', self.copy)
            self.app.bind('<Command-v>', self.paste)
        self.grid()

    def update(self):
        ''' on change frames '''
        # self.grid()
        #NOTE this is called during zoom and pan
        #this means the crosshairs are redrawn for every <Motion> call, which is a lot
        #we could probably just move them instead
        self.reset()  # clear our crosshairs
        self.read()  # read from file
        #self.frame.update()
        #debug("TraceModule", self.frame.winfo_width())
    def reset(self):
        ''' on change files '''
        # undraw all the crosshairs
        for trace in self.crosshairs:
            for ch in self.crosshairs[trace]:
                ch.undraw()
        # and empty out our trackers
        self.crosshairs = {}
        self.selected = set()

    def add(self, x, y, _trace=None, transform=True):
        '''
        add a crosshair to the zoom frame canvas
        '''

        trace = self.getCurrentTraceName() if _trace == None else _trace
        color = self.available[trace]['color']
        ch = Crosshairs(self.app.Dicom.zframe, x, y, color, transform)
        if trace not in self.crosshairs:
            self.crosshairs[trace] = []
        self.crosshairs[trace].append(ch)
        return ch

    def remove(self, ch, write=True):
        '''
        remove a crosshair from the zoom frame canvas ... doesn't actually remove it
        but instead just makes it "invisible"
        '''

        ch.undraw()
        if write:
            self.write()
        return ch

    def move(self):
        ''' called when window resizes to move to correct relative locations'''
        # trace = self.getCurrentTraceName()
        if self.crosshairs:
            for trace in self.crosshairs:
                for ch in self.crosshairs[trace]:
                    truex, truey = ch.getTrueCoords()
                    ch.x, ch.y = ch.transformTrueToCoords(truex, truey)
                    ch.dragTo((ch.x, ch.y))

    def read(self):
        '''
        read a list of crosshair coordinates from the metadata file
        '''
        frame = self.app.frame
        for trace in self.available:
            try:
                newCrosshairs = []
                for item in self.app.Data.getTraceCurrentFrame(trace):
                    ch = self.add(item['x'],
                                  item['y'],
                                  _trace=trace,
                                  transform=False)
                    if trace not in self.crosshairs:
                        self.crosshairs[trace] = []
                    self.crosshairs[trace].append(ch)
                    newCrosshairs.append(ch)
                self.app.Control.push({'type': 'add', 'chs': newCrosshairs})
            except KeyError:
                pass

    def write(self):
        '''
        write out the coordinates of all of our crosshairs to the metadata file:
        '''

        trace = self.getCurrentTraceName()
        traces = []

        # prepare trace data in format for metadata array
        if trace in self.crosshairs:
            for ch in self.crosshairs[trace]:
                if ch.isVisible:
                    x, y = ch.getTrueCoords()
                    data = {'x': x, 'y': y}
                    if data not in traces:
                        # add trace to temporary array for including in metadata array
                        traces.append(data)
        # add to metadata array and update file
        self.app.Data.setCurrentTraceCurrentFrame(traces)
        # update tier labels for number of annotated frames
        self.app.TextGrid.updateTierLabels()

    def getCurrentTraceName(self):
        '''
        return string of current trace name
        '''

        try:
            return self.listbox.get(self.listbox.curselection())
        except Exception as e:  # tkinter.TclError?
            error('Can\'t select from empty listbox!', e)

    def setDefaultTraceName(self):
        '''
        wrapper for changing the default trace
        '''
        self.app.Data.setTopLevel('defaultTraceName',
                                  self.getCurrentTraceName())

    def select(self, ch):
        ''' select a crosshairs '''
        ch.select()
        self.selected.add(ch)

    def selectAll(self):
        ''' select all crosshairs '''
        if self.getCurrentTraceName() in self.crosshairs:
            for ch in self.crosshairs[self.getCurrentTraceName()]:
                self.select(ch)

    def unselect(self, ch):
        ''' unselect a crosshairs '''
        ch.unselect()
        self.selected.remove(ch)

    def unselectAll(self):
        ''' unselect all crosshairs '''
        for ch in self.selected:
            ch.unselect()
        self.selected = set()

    def getNearClickAllTraces(self, click):
        '''
        takes a click object ( (x,y) tuple ) and returns a list of crosshairs
        within _CROSSHAIR_SELECT_RADIUS

        first searches for crosshairs matching the current trace iterates
        thru the other traces if it doesnt find anything

        if nothing is found for any trace, returns None
        '''
        # get nearby crosshairs from this trace
        nearby = self.getNearClickOneTrace(click, self.getCurrentTraceName())
        if nearby != None:
            return nearby

        # otherwise
        else:
            # ... check our other traces to see if they contain any nearby guys
            for trace in self.available:
                nearby = self.getNearClickOneTrace(click, trace)
                # if we got something
                if nearby != None:
                    # switch to that trace and exit the loop
                    for i, item in enumerate(self.listbox.get(0, 'end')):
                        if item == trace:
                            self.listbox.selection_clear(0, 'end')
                            self.listbox.select_set(i)
                    return nearby

        return None

    def getNearClickOneTrace(self, click, trace):
        '''
        takes a click object and a trace and returns a list of crosshairs within
        util.CROSSHAIR_SELECT_RADIUS of that click
        '''

        # see if we clicked near any existing crosshairs
        possibleSelections = {}
        if trace in self.crosshairs:
            for ch in self.crosshairs[trace]:
                d = ch.getDistance(click)
                if d < util.CROSSHAIR_SELECT_RADIUS:
                    if d in possibleSelections:
                        possibleSelections[d].append(ch)
                    else:
                        possibleSelections[d] = [ch]

        # if we did ...
        if possibleSelections != {}:
            # ... get the closest one ...
            dMin = sorted(possibleSelections.keys())[0]
            # ... in case of a tie, select a random one
            ch = random.choice(possibleSelections[dMin])
            return ch

        return None

    def copy(self, event=None):
        ''' copies relative positions of selected crosshairs for pasting'''
        # debug('copy')
        self.copied = []
        if len(self.selected) > 0:
            for ch in self.selected:
                self.copied.append(ch.getTrueCoords())

    def paste(self, event=None):
        ''' pastes copied crosshairs and add them to undo/redo buffer '''
        if len(self.copied) > 0:
            newChs = []
            for xy in self.copied:
                ch = self.add(xy[0], xy[1], transform=False)
                newChs.append(ch)
            self.write()
            self.app.Control.push({'type': 'add', 'chs': newChs})

    def recolor(self, event=None, trace=None, color=None):
        ''' change the color of a particular trace '''

        trace = self.getCurrentTraceName() if trace == None else trace

        # grab a new color and save our old color (for generating Control data)
        newColor = self.getRandomHexColor() if color == None else color
        oldColor = self.app.Data.getCurrentTraceColor()

        self.available[trace]['color'] = newColor
        self.app.Data.setTraceColor(trace, newColor)

        if trace in self.crosshairs:
            for ch in self.crosshairs[trace]:
                ch.recolor(newColor)

        if trace == None or color == None:
            self.app.Control.push({
                'type': 'recolor',
                'trace': self.getCurrentTraceName(),
                'color': oldColor
            })
            self.redoQueue = []
            # FIXME: get this to update the widget
            self.app.Trace.displayedColour = newColor
            # FIXME: also get the widget to update the colour!

        return oldColor

    def clear(self):
        ''' remove all crosshairs for the current trace '''

        # now we remove all the traces and save
        deleted = []
        trace = self.getCurrentTraceName()
        if trace in self.crosshairs:
            for ch in self.crosshairs[trace]:
                if ch.isVisible:
                    deleted.append(ch)
                self.remove(ch, write=False)
            self.write()

        self.app.Control.push({'type': 'delete', 'chs': deleted})

    def newTrace(self):
        ''' add a new trace to our listbox '''

        # max length 12 chars (so it displays nicely)
        name = self.traceSV.get()[:12]

        # don't want to add traces we already have or empty strings
        if name not in self.available and len(name) > 0:

            # choose a random color
            color = self.getRandomHexColor()

            # save the new trace name and color to metadata & update vars
            self.available[name] = {'color': color, 'files': {}}
            self.app.Data.setTopLevel('traces', self.available)
            self.traceSV.set('')

            # update our listbox
            self.listbox.insert('end', name)
            self.listbox.selection_clear(0, 'end')
            self.listbox.select_set(len(self.available) - 1)

    def renameTrace(self, oldName=None, newName=None):
        ''' change a trace name from oldName -> newName '''

        fromUndo = (oldName != None or newName != None)
        oldName = self.getCurrentTraceName() if oldName == None else oldName
        newName = self.traceSV.get()[:12] if newName == None else newName

        # don't overwrite anything
        if newName not in self.available and len(newName) > 0:

            # get data from the old name and change the dictionary key in the metadata
            data = self.available.pop(oldName)
            self.available[newName] = data
            self.app.Data.setTopLevel('traces', self.available)
            if oldName == self.app.Data.getTopLevel('defaultTraceName'):
                self.app.Data.setTopLevel('defaultTraceName', newName)
            self.traceSV.set('')

            # update our listbox
            index = self.listbox.curselection()
            self.listbox.delete(index)
            self.listbox.insert(index, newName)
            self.listbox.selection_clear(0, 'end')
            self.listbox.select_set(index)

            if not (fromUndo):
                self.app.Control.push({
                    'type': 'rename',
                    'old': oldName,
                    'new': newName
                })

    def getRandomHexColor(self):
        ''' helper for getting a random color '''
        return '#%06x' % random.randint(0, 0xFFFFFF)

    def getWidget(self,
                  widget,
                  row=0,
                  column=0,
                  rowspan=1,
                  columnspan=1,
                  sticky=()):
        ''' helper for managing all of our widgets '''
        return {
            'widget': widget,
            'row': row,
            'rowspan': rowspan,
            'column': column,
            'columnspan': columnspan,
            'sticky': sticky
        }

    def grid(self):
        ''' grid all of our widgets '''
        for item in self.TkWidgets:
            item['widget'].grid(row=item['row'],
                                column=item['column'],
                                rowspan=item['rowspan'],
                                columnspan=item['columnspan'],
                                sticky=item['sticky'])
        self.listbox.pack(side='left', fill='y')
        self.scrollbar.pack(side='right', fill='y')

    def grid_remove(self):
        ''' remove all of our widgets from the grid '''
        for item in self.TkWidgets:
            item['widget'].grid_remove()
        self.listbox.packforget()
        self.scrollbar.packforget()