<commit_before>from __future__ import print_function

from collections import defaultdict
import math
import sys

from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from PyQt4.QtCore import Qt
from PyQt4.QtGui import (
    QBrush, QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QMainWindow,
    QPainter, QVBoxLayout, QWidget)
import numpy as np

from vesper.archive.archive import Archive
from vesper.ui.clip_times_rug_plot import ClipTimesRugPlot
from vesper.ui.flow_layout import FlowLayout
from vesper.ui.multiselection import Multiselection
from vesper.ui.spectrogram_clip_figure import \
    SpectrogramClipFigure as ClipFigure
from vesper.util.bunch import Bunch
from vesper.util.preferences import preferences as prefs
from vesper.util.preferences import preset_manager
import vesper.util.classification_command_utils as command_utils


_SPACING_ASPECT_RATIO = 2
"""ratio of vertical clip spacing to minimum horizontal spacing."""


class ClipsWindow(QMainWindow):
    
    
    def __init__(
            self, parent, archive, station_name, detector_name, night,
            clip_class_name, commands_preset_name):
        
        super(ClipsWindow, self).__init__(parent)
        
        self.setAttribute(Qt.WA_DeleteOnClose)
        
        self._archive = archive
        
        self._create_ui()
        
        self._init_commands(commands_preset_name)
        
        self.set_clips(station_name, detector_name, night, clip_class_name)
        
        date = str(self._date.day) + ' ' + self._date.strftime('%B %Y')
        title = '{:s} - {:s} - {:s} - {:s}'.format(
            self._archive.name, self._station_name, self._detector_name, date)
        self.setWindowTitle(title)
        
                
    def _create_ui(self):
        parent = QWidget(self)
        self._create_ui_components(parent)
        self._lay_out_ui_components(parent)
        
        
    def _create_ui_components(self, parent):
        
        (self._commands_combo_box, self._commands_indices,
         self._commands_presets) = \
            _create_preset_combo_box(
                self, 'Classification Commands', self._on_commands_changed)
        self._commands_combo_box.setFocusPolicy(Qt.NoFocus)

        self._title_label = QLabel(parent)
        self._title_label.setAlignment(Qt.AlignCenter)

        self._rug_plot = ClipTimesRugPlot(parent, self.move_to_page)
        
        config = Bunch(
            clips_area_width=prefs['clipsWindow.duration'],
            clip_spacing=prefs['clipsWindow.spacing'],
            num_rows=prefs['clipsWindow.numRows'],
            min_clip_height=40,
            selection_rect_thickness=3,
            selection_rect_color='red'
        )
        self._figures_frame = _FiguresFrameWithFlowLayout(parent, config)
        
        
    def _lay_out_ui_components(self, parent):
        
        grid = QGridLayout()
        grid.setContentsMargins(20, 0, 20, 0)
        grid.addWidget(self._title_label, 0, 1, Qt.AlignHCenter)
        
        if self._commands_combo_box is not None:
            box = QHBoxLayout()
            box.addWidget(QLabel('Commands:', parent))
            box.addWidget(self._commands_combo_box)
            grid.addLayout(box, 0, 2, Qt.AlignRight)
            
        for i in xrange(3):
            grid.setColumnMinimumWidth(i, 10)
            grid.setColumnStretch(i, 1)
        
        hBox = QHBoxLayout()
        hBox.addSpacing(20)
        hBox.addWidget(self._rug_plot.canvas)
        hBox.addSpacing(20)
        
        box = QVBoxLayout()
        box.addLayout(grid)
        box.addLayout(hBox)
        box.addWidget(self._figures_frame, stretch=1)
        
        parent.setLayout(box)
        
        # We set the left, right, and bottom margins of the box layout
        # to zero since we will lay out the figures of the figure frame
        # ourselves. It seems to be important to set the margins after
        # the call to `self.setLayout` rather than before since before
        # the call the margins are all zero.
        margins = box.contentsMargins()
        margins.setLeft(0)
        margins.setRight(0)
        margins.setBottom(0)
        box.setContentsMargins(margins)

        self.setCentralWidget(parent)

    
    def _on_commands_changed(self, index):
        self._update_classification_dict(index)
        
        
    def _update_classification_dict(self, index):
        preset = self._commands_presets[index]
        self._classification_dict = \
            self._create_classification_dict(preset)
        
        
    def _create_classification_dict(self, commands_preset):
        
        """
        Creates a mapping from command names to clip classes for the
        specified commands preset.
        
        The mapping is the composition of the preset's commands dictionary,
        which maps command names to clip class name fragments, and a mapping
        from clip class name fragments to clip classes. For each clip class
        name fragment of a command, there must be exactly one clip class
        whose name ends with that fragment.
        """
        
        commands = commands_preset.commands
        classes = self._archive.clip_classes
        classes_dict = self._create_fragment_to_classes_dict(classes)
        classification_dict = {}
        
        for command_name, (fragment, all_) in commands.iteritems():
            
            classes = classes_dict.get(fragment)
            
            if classes is None:
                # no clip class names include fragment
                
                # TODO: Implement a proper logging facility.
                print((
                    'Warning: Unrecognized clip class name fragment "{}" '
                    'for command "{}" in {} preset "{}". Command will be '
                    'ignored.').format(
                        fragment, command_name, commands_preset.type_name,
                        commands_preset.name),
                    file=sys.stderr)
            
            elif len(classes) != 1:
                # more than one clip class name includes fragment
                
                # Look for clip class whose entire name is fragment.
                clip_class = None
                for c in classes:
                    if c.name == fragment:
                        clip_class = c
                        
                if clip_class is None:
                    # no clip class has name equal to fragment
                    
                    # TODO: Implement a proper logging facility.
                    print((
                        'Warning: Clip class name fragment "{}" for '
                        'command "{}" in {} preset "{}" is ambiguous. '
                        'Command will be ignored.').format(
                            fragment, command_name, commands_preset.type_name,
                            commands_preset.name),
                        file=sys.stderr)
                
                else:
                    # one clip class has name equal to fragment
                    
                    classification_dict[command_name] = (clip_class, all_)
            
            else:
                # exactly one clip class for specified fragment
                
                classification_dict[command_name] = (classes[0], all_)
            
        return classification_dict
        
        
    def _create_fragment_to_classes_dict(self, clip_classes):
        d = defaultdict(list)
        d[Archive.CLIP_CLASS_NAME_UNCLASSIFIED].append(None)
        for c in clip_classes:
            for f in self._get_clip_class_name_fragments(c.name):
                d[f].append(c)
        return d
    
    
    def _get_clip_class_name_fragments(self, name):
        sep = Archive.CLIP_CLASS_NAME_COMPONENT_SEPARATOR
        parts = name.split(sep)
        n = len(parts)
        return [sep.join(parts[i:n]) for i in xrange(n)]
    
    
    def _init_commands(self, preset_name):
        
        if self._commands_combo_box is not None:
            
            index = 0
            
            if preset_name is not None:
                
                try:
                    index = self._commands_indices[preset_name]
                    
                except KeyError:
                    # TODO: Log error.
                    pass
                    
            if self._commands_combo_box.currentIndex() == index:
                self._update_classification_dict(index)
                
            else:
                self._commands_combo_box.setCurrentIndex(index)
                
        else:
            # no commands presets
            
            self._classification_dict = {}
        
        
    def set_clips(self, station_name, detector_name, date, clip_class_name):

        # TODO: Does this make sense? Think about what `None` arguments
        # mean and whether or not we actually support them (it seems like
        # we currently don't, except perhaps for the clip class name).
        if station_name is None or detector_name is None or \
           date is None or clip_class_name is None:
            
            self._clips = []
            
        else:
            # Update clips.
            # TODO: Handle `getClips` failure gracefully.
            # TODO: Get clips on a separate thread so UI thread can
            # keep running. Set clips on self._figures_frame on UI thread
            # when that thread completes.
            print('getting clips from archive...')
            self._clips = self._archive.get_clips(
                station_name, detector_name, date, clip_class_name)
        
        self._station_name = station_name
        self._detector_name = detector_name
        self._date = date
        self._clip_class_name = clip_class_name
        
        print('updating UI...')
        self._update_ui()
        
        print('done')
        
        
    def _update_ui(self):
        frame = self._figures_frame
        frame.clips = self._clips
        self._rug_plot.set_clips(self._clips, frame.page_start_indices)
        self._update_title()
        

    def _update_title(self):
        
        if self._station_name is None or self._detector_name is None or \
           self._date is None or self._clip_class_name is None:
            
            title = 'No Clips'
            
        else:
            title = self._create_clips_string()

        self._rug_plot.current_page_num = self._figures_frame.page_num
        self._title_label.setText(title)

        
    def _create_clips_string(self):
        
        frame = self._figures_frame
        n = len(self._clips)
        
        if n == 0:
            return 'No {:s} Clips'.format(self._clip_class_name)
        
        else:
            
            first = frame.first_visible_clip_num
            last = min(first + frame.num_visible_clips - 1, n - 1)
            
            if first == last:
                return '{:s} Clip {:d} of {:d}'.format(
                    self._clip_class_name, first + 1, n)
            
            else:
                return '{:s} Clips {:d}-{:d} of {:d}'.format(
                    self._clip_class_name, first + 1, last + 1, n)
    
        
    def keyPressEvent(self, e):
        
        command = command_utils.get_command_from_key_event(e)
        
        if command is not None:
            
            pair = self._classification_dict.get(command)
        
            if pair is not None:
                self.classify(*pair)
                
            else:
                # key is not a classification command
                
                self._key_press_event(e)
                
        
        else:
            # key is not a classification command
            
            self._key_press_event(e)
            
            
    def _key_press_event(self, e):
        
        is_key = command_utils.is_key
        
        if is_key(e, Qt.Key_Space) or is_key(e, Qt.Key_PageDown):
            self.move_down_one_page()
            
        elif is_key(e, Qt.Key_Space, Qt.ShiftModifier) or \
                is_key(e, Qt.Key_PageUp):
            
            self.move_up_one_page()
            
        elif is_key(e, Qt.Key_Tab):
            self.move_singleton_selection_forward()
            
        # Note that when the user presses the tab key with the shift
        # key held down, the event we get is for the backtab key
        # rather than the tab key, with the shift modifier.
        elif is_key(e, Qt.Key_Backtab, Qt.ShiftModifier):
            self.move_singleton_selection_backward()
            
        else:
            super(ClipsWindow, self).keyPressEvent(e)

    
    @property
    def classification_dict(self):
        return self._classification_dict
        
        
    def classify(self, clip_class, scope):
        self._figures_frame.classify(clip_class, scope)
        self._update_title()
        

    def move_down_one_page(self):
        self._figures_frame.move_down_one_page()
        self._update_title()
                        
            
    def move_up_one_page(self):
        self._figures_frame.move_up_one_page()
        self._update_title()
        
        
    def move_to_page(self, page_num):
        self._figures_frame.move_to_page(page_num)
        self._update_title()
        
        
    def move_singleton_selection_forward(self):
        self._figures_frame.move_singleton_selection_forward()
        self._update_title()


    def move_singleton_selection_backward(self):
        self._figures_frame.move_singleton_selection_backward()
        self._update_title()


class _FiguresFrame(QWidget):
    
    
    def __init__(self, parent, config):
        
        super(_FiguresFrame, self).__init__(parent)

#        self.setStyleSheet('QFrame { background-color: red }')
        
        self._num_rows = config.num_rows
        self._selection_rect_thickness = config.selection_rect_thickness
        self._selection_rect_color = config.selection_rect_color
        
        self._first_visible_row_num = None
        self._inactive_clip_frames = []
        self._active_clip_frames = []
        
        # We leave the initial setting of `self._clips` and
        # `self._selection` to subclass initializers since that
        # process often uses subclass instance data that have not
        # yet been initialized when this method runs.


    @property
    def num_rows(self):
        return self._num_rows


    @property
    def clips(self):
        return self._clips
    
    
    @clips.setter
    def clips(self, clips):
        # self._show_clip_durs(clips)
        self._set_clips(clips)


    def _show_clip_durs(self, clips):
        sounds = [c.sound for c in clips]
        durs = [len(s.samples) / s.sample_rate for s in sounds]
        durs.sort()
        if len(durs) > 0:
            print('_FigureFrame', durs[0], durs[-1])
            
           
    def _set_clips(self, clips):
        raise NotImplementedError()
    
    
    @property
    def selection(self):
        return self._selection
    
    
    @property
    def total_num_rows(self):
        raise NotImplementedError()
        
        
    @property
    def max_first_visible_row_num(self):
        if self.total_num_rows == 0:
            return None
        else:
            return max(0, self.total_num_rows - self.num_rows)
    
    
    @property
    def first_visible_row_num(self):
        return self._first_visible_row_num
            
        
    @first_visible_row_num.setter
    def first_visible_row_num(self, n):
        if self.total_num_rows != 0:
            self._set_first_visible_row_num(n)
        
        
    def _set_first_visible_row_num(self, n, force_update=False):
        
        n = max(n, 0)
        n = min(n, self.total_num_rows - 1)
        
        if n != self._first_visible_row_num or force_update:
            
            self._first_visible_row_num = n
            
            clip_num = self._get_first_clip_num_of_row(n)
            clips = self._clips[clip_num:clip_num + self.num_visible_clips]
            
            self._activate_clip_frames(clips)
            
    
    @property
    def first_visible_clip_num(self):
        n = self.first_visible_row_num
        return self._get_first_clip_num_of_row(n) if n is not None else None
    
    
    def _get_first_clip_num_of_row(self, n):
        raise NotImplementedError()
    
    
    @property
    def num_visible_clips(self):
        
        if self.total_num_rows == 0:
            return 0
        
        else:
            # have some clips
            
            clip_num = self._get_first_clip_num_of_row(
                self.first_visible_row_num + self.num_rows)
            
            if clip_num is None:
                return len(self._clips) - self.first_visible_clip_num
            
            else:
                return clip_num - self.first_visible_clip_num
    
    
    @property
    def num_pages(self):
        return int(math.ceil(self.total_num_rows / float(self.num_rows)))
    
    
    @property
    def page_start_indices(self):
        return [self._get_first_clip_num_of_row(i * self.num_rows)
                for i in xrange(self.num_pages)]
        
        
    @property
    def page_num(self):
        clip_num = self.first_visible_clip_num
        if clip_num is None:
            return None
        else:
            return np.searchsorted(
                self.page_start_indices, clip_num, side='right') - 1
            
            
    def classify(self, clip_class, scope):
        
        new_name = clip_class.name if clip_class is not None else None
        
        if scope == 'All':
            intervals = ((0, len(self._clips) - 1),)
            self._classify(intervals, new_name)
            
        else:
            
            if self.num_visible_clips == 0:
                return
            
            first = self.first_visible_clip_num
            
            if scope == 'Page':
                intervals = ((first, first + self.num_visible_clips - 1),)
            else:
                intervals = _shift(self.selection.selected_intervals, first)
                
            self._classify(intervals, new_name)
            
            if prefs['clipsWindow.advanceAfterClassification']:
                self._advance_after_classification(scope)
                
                            
    def _classify(self, intervals, new_name):
        
        first = self.first_visible_clip_num
            
        for i, j in intervals:
            
            for k in xrange(i, j + 1):
                
                clip = self._clips[k]
                
                old_name = clip.clip_class_name
                
                if new_name != old_name:
                    
                    clip.clip_class_name = new_name
                    
                    if k >= first and k < first + self.num_visible_clips:
                    
                        # This is very naughty. The figures frame should not
                        # be rooting around in clip frames' private parts and
                        # telling their clip figures to update their clip text.
                        # Instead, clips (as models) should probably support
                        # observers and clip figures (as views of clips) should
                        # update themselves when they are notified that their
                        # clips' classifications have changed.
                        frame = self._active_clip_frames[k - first]
                        figure = frame._clip_figure
                        figure._update_clip_text()
                    

    def _advance_after_classification(self, scope):
        
        if scope == 'Selected':
            self.move_singleton_selection_forward()
                
        elif scope == 'Page':
                    
            last_visible_clip_num = \
                self.first_visible_clip_num + self.num_visible_clips - 1
            
            if last_visible_clip_num != len(self._clips) - 1:
                # there are more clips
                
                self.move_down_one_page()
                
        
    def move_down_one_page(self):
        if self.first_visible_row_num + self.num_rows < self.total_num_rows:
            self._move_by_num_rows(self.num_rows)
        
    
    def _move_by_num_rows(self, num_rows):
        if self.total_num_rows != 0:
            self.first_visible_row_num += num_rows
            
            
    def move_up_one_page(self):
        self._move_by_num_rows(-self.num_rows)
        
    
    def move_to_page(self, page_num):
        if self.total_num_rows != 0:
            if page_num < 0:
                page_num = 0
            elif page_num >= self.num_pages:
                page_num = self.num_pages - 1
            self.first_visible_row_num = page_num * self.num_rows
            
            
    def move_singleton_selection_forward(self):
        
        intervals = self.selection.selected_intervals
        
        if _is_singleton(intervals):
            
            clip_num = self.first_visible_clip_num + intervals[0][0]
            
            last_visible_clip_num = \
                self.first_visible_clip_num + self.num_visible_clips - 1
            
            if clip_num != last_visible_clip_num:
                # selected clip is not last on this page
                
                self._select_clip(clip_num + 1)

            elif clip_num != len(self._clips) - 1:
                # there are clips after this page
                
                self.move_down_one_page()
                self._select_clip(clip_num + 1)
                
                
    def _select_clip(self, clip_num):
        
        self.selection.select(clip_num - self.first_visible_clip_num)
        
        # TODO: Should selections be observable, so that we
        # can observe the selection and update our graphical
        # representation of it when it changes?
        self._update_clip_frame_selection_states()
        

    def _update_clip_frame_selection_states(self):
        for frame in self._active_clip_frames:
            frame.selected = frame.index in self.selection


    def move_singleton_selection_backward(self):

        intervals = self.selection.selected_intervals
        
        if _is_singleton(intervals):
            
            clip_num = self.first_visible_clip_num + intervals[0][0]
            
            if clip_num != self.first_visible_clip_num:
                # selected clip is not first on this page
                
                self._select_clip(clip_num - 1)

            elif clip_num != 0:
                # there are clips before this page
                
                self.move_up_one_page()
                self._select_clip(clip_num - 1)
                
                
    def _handle_selection_event(self, figure_frame, event):
        
        index = figure_frame.index
        selection = self._selection
        
        if _control_down(event):
            selection.toggle(index)
            
        elif _shift_down(event):
            selection.extend(index)
            
        else:
            selection.select(index)
            
        self._update_clip_frame_selection_states()
            
        self.setFocus()


def _shift(intervals, n):
    return tuple([(i + n, j + n) for i, j in intervals])


def _is_singleton(intervals):
    
    if len(intervals) != 1:
        return False
    
    else:
        start, end = intervals[0]
        return start == end
    

class _FiguresFrameWithFlowLayout(_FiguresFrame):
    
    
    def __init__(self, parent, config, **kwargs):
        
        super(_FiguresFrameWithFlowLayout, self).__init__(
            parent, config, **kwargs)
        
        # This helps prevent clip frames for short clips from winding
        # up with zero width, which causes problems on Windows. It is
        # not really a solution to that problem. It can be removed
        # when issue #31 is addressed.
        self.setMinimumSize(500, 300)
        
        # clip layout parameters
        self._clips_area_width = config.clips_area_width
        self._clip_spacing = config.clip_spacing
        self._min_clip_height = config.min_clip_height
        
        self.clips = []
        
        
    def _set_clips(self, clips):
        
        self._clips = clips
        
        self._layout = self._assign_clips_to_rows()

        if self.total_num_rows == 0:
            self._deactivate_clip_frames()
            self._first_visible_row_num = None
            self._selection = None
            
        else:
            self._set_first_visible_row_num(0, force_update=True)
        
            
    def _assign_clips_to_rows(self):
        durations = [clip.duration for clip in self._clips]
        width = self._clips_area_width
        layout = FlowLayout(width, width * self._clip_spacing)
        return layout.lay_out_items(durations)
        
        
    def _deactivate_clip_frames(self):
        
        for frame in self._active_clip_frames:
            frame.hide()
            frame.clip = None
            frame.selected = False
            
        self._inactive_clip_frames += self._active_clip_frames
        self._active_clip_frames = []
        
        
    def _activate_clip_frames(self, clips):
        
        self._deactivate_clip_frames()
        
        m = self._first_visible_row_num
        num_frames = sum(n for (_, n) in self._layout[m:m + self.num_rows])
        
        inactive_frames = self._inactive_clip_frames
        n = num_frames - len(inactive_frames)
        if n > 0:
            print('    creating clip frames...')
            thickness = self._selection_rect_thickness
            color = self._selection_rect_color
            for _ in xrange(n):
                frame = _FigureFrame(self, thickness, color)
                inactive_frames.append(frame)
        
        print('    setting clips on frames...')
        for i in xrange(num_frames):
            frame = inactive_frames.pop()
            frame.clip = clips[i]
            frame.index = i
#            frame.show()
            self._active_clip_frames.append(frame)
        
        print('    creating selection...')
        self._selection = Multiselection(0, len(clips) - 1)
        
        print('    laying out clip frames...')
        self._lay_out_clips()
        
        print('    showing clip frames...')
        self._show_clip_frames()
        
        print('    done')

    
    # This is a Qt event handler, so don't rename it!
    def resizeEvent(self, event):
        self._lay_out_clips()
            
            
    def _lay_out_clips(self):
        
        if len(self._clips) == 0:
            return
        
        size = self.size()
        clips_frame_width = size.width()
        clips_frame_height = size.height()
        
        min_spacing = 2 * self._selection_rect_thickness
        h_spacing = max(clips_frame_width * self._clip_spacing, min_spacing)
        v_spacing = max(
            clips_frame_height * self._clip_spacing * _SPACING_ASPECT_RATIO,
            min_spacing)
        
        (ys, height) = self._lay_out_clip_rows(clips_frame_height, v_spacing)
        
        num_rows = min(
            self.num_rows, self.total_num_rows - self._first_visible_row_num)
        frame_num = 0
        border_sizes = []
                
        for i in xrange(num_rows):
            
            row_num = self._first_visible_row_num + i
            
            (start_clip_num, num_clips) = self._layout[row_num]
            
            # The `max(clips_frame_width, 1)` in the following prevents
            # division by zero.
            seconds_per_pixel = \
                float(self._clips_area_width) / max(clips_frame_width, 1)
            
            clip_widths = [
                int(round(self._clips[j].duration / seconds_per_pixel))
                for j in xrange(start_clip_num, start_clip_num + num_clips)]
            
            # Under this spacing regime, the space between clips is
            # `h_spacing` and the remaining space is divided equally
            # between the left and right borders.
#             spacing = h_spacing
#             borderSize = (clips_frame_width - sum(clip_widths) - \
#                           (num_clips - 1) * spacing) / 2.
#             borderSize = max(borderSize, min_spacing)
            
            # Under this spacing regime, the space in a row is nominally
            # divided evenly between the borders and the spaces between
            # clips. Rows for which this would cause the spacing to be
            # undesirably large are treated specially.
            space = clips_frame_width - sum(clip_widths)
            spacing = space / float(num_clips + 1)
            spacing = max(spacing, min_spacing)
            border_size = spacing
            if spacing >= 5 * h_spacing:
                spacing = 3 * h_spacing
                if row_num != self.total_num_rows - 1:
                    border_size = (space - (num_clips - 1) * spacing) / 2.
                else:
                    sizes = np.array(border_sizes)
                    if len(sizes) == 0:
                        border_size = spacing
                    else:
                        border_size = np.median(sizes)
            border_sizes.append(border_size)

            clip_x = border_size
            y = ys[i]
            
            for i in xrange(num_clips):
                
                x = int(round(clip_x - self._selection_rect_thickness))
                width = clip_widths[i]
                
                frame = self._active_clip_frames[frame_num]
                frame.setGeometry(x, y, width, height)
                
                clip_x += width + spacing
                
                frame_num += 1
                
                
    def _lay_out_clip_rows(self, clips_frame_height, spacing):
        
        min_height = self._min_clip_height
        thickness = self._selection_rect_thickness
        
        total_height = (clips_frame_height - (self.num_rows + 1) * spacing)
        clip_height = max(total_height / float(self.num_rows), min_height)
        clip_frame_height = int(round(clip_height)) + 2 * thickness

        offsets = (clip_height + spacing) * np.arange(self.num_rows)
        ys = spacing - thickness + offsets
        ys = np.array(ys, dtype=int)
        
        return (ys, clip_frame_height)
        

    @property
    def total_num_rows(self):
        return len(self._layout)
    
    
    def _get_first_clip_num_of_row(self, n):
        if n < 0 or n >= self.total_num_rows:
            return None
        else:
            return self._layout[n][0]
        
        
    def _show_clip_frames(self):
        for frame in self._active_clip_frames:
            frame.show()
        
        
class _FigureFrame(QFrame):
    
    """
    Frame that displays one clip.
    
    The frame contains an inset spectrogram and a border whose color
    indicates the selection status of the clip.
    """
    
    
    def __init__(self, parent, selection_rect_thickness, selection_rect_color):
        
        super(_FigureFrame, self).__init__(parent)
        
        self._index = None
        
        self._selection_rect_thickness = selection_rect_thickness
        self._selection_rect_color = selection_rect_color
        
        self._brush = QBrush(selection_rect_color)
        
        self.selected = False
        
        figure = Figure()
        canvas = FigureCanvasQTAgg(figure)
        canvas.setParent(self)
        self._clip_figure = ClipFigure(self, figure)

        
    @property
    def clip(self):
        return self._clip_figure.clip
    
    
    @clip.setter
    def clip(self, clip):
        self._clip_figure.clip = clip
        
        
    @property<commit_msg>Fixed Qt problem that appeared after a recent update.<commit_after>from __future__ import print_function

from collections import defaultdict
import math
import sys

from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from PyQt4.QtCore import Qt
from PyQt4.QtGui import (
    QBrush, QColor, QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel,
    QMainWindow, QPainter, QVBoxLayout, QWidget)
import numpy as np

from vesper.archive.archive import Archive
from vesper.ui.clip_times_rug_plot import ClipTimesRugPlot
from vesper.ui.flow_layout import FlowLayout
from vesper.ui.multiselection import Multiselection
from vesper.ui.spectrogram_clip_figure import \
    SpectrogramClipFigure as ClipFigure
from vesper.util.bunch import Bunch
from vesper.util.preferences import preferences as prefs
from vesper.util.preferences import preset_manager
import vesper.util.classification_command_utils as command_utils


_SPACING_ASPECT_RATIO = 2
"""ratio of vertical clip spacing to minimum horizontal spacing."""


class ClipsWindow(QMainWindow):
    
    
    def __init__(
            self, parent, archive, station_name, detector_name, night,
            clip_class_name, commands_preset_name):
        
        super(ClipsWindow, self).__init__(parent)
        
        self.setAttribute(Qt.WA_DeleteOnClose)
        
        self._archive = archive
        
        self._create_ui()
        
        self._init_commands(commands_preset_name)
        
        self.set_clips(station_name, detector_name, night, clip_class_name)
        
        date = str(self._date.day) + ' ' + self._date.strftime('%B %Y')
        title = '{:s} - {:s} - {:s} - {:s}'.format(
            self._archive.name, self._station_name, self._detector_name, date)
        self.setWindowTitle(title)
        
                
    def _create_ui(self):
        parent = QWidget(self)
        self._create_ui_components(parent)
        self._lay_out_ui_components(parent)
        
        
    def _create_ui_components(self, parent):
        
        (self._commands_combo_box, self._commands_indices,
         self._commands_presets) = \
            _create_preset_combo_box(
                self, 'Classification Commands', self._on_commands_changed)
        self._commands_combo_box.setFocusPolicy(Qt.NoFocus)

        self._title_label = QLabel(parent)
        self._title_label.setAlignment(Qt.AlignCenter)

        self._rug_plot = ClipTimesRugPlot(parent, self.move_to_page)
        
        config = Bunch(
            clips_area_width=prefs['clipsWindow.duration'],
            clip_spacing=prefs['clipsWindow.spacing'],
            num_rows=prefs['clipsWindow.numRows'],
            min_clip_height=40,
            selection_rect_thickness=3,
            selection_rect_color='red'
        )
        self._figures_frame = _FiguresFrameWithFlowLayout(parent, config)
        
        
    def _lay_out_ui_components(self, parent):
        
        grid = QGridLayout()
        grid.setContentsMargins(20, 0, 20, 0)
        grid.addWidget(self._title_label, 0, 1, Qt.AlignHCenter)
        
        if self._commands_combo_box is not None:
            box = QHBoxLayout()
            box.addWidget(QLabel('Commands:', parent))
            box.addWidget(self._commands_combo_box)
            grid.addLayout(box, 0, 2, Qt.AlignRight)
            
        for i in xrange(3):
            grid.setColumnMinimumWidth(i, 10)
            grid.setColumnStretch(i, 1)
        
        hBox = QHBoxLayout()
        hBox.addSpacing(20)
        hBox.addWidget(self._rug_plot.canvas)
        hBox.addSpacing(20)
        
        box = QVBoxLayout()
        box.addLayout(grid)
        box.addLayout(hBox)
        box.addWidget(self._figures_frame, stretch=1)
        
        parent.setLayout(box)
        
        # We set the left, right, and bottom margins of the box layout
        # to zero since we will lay out the figures of the figure frame
        # ourselves. It seems to be important to set the margins after
        # the call to `self.setLayout` rather than before since before
        # the call the margins are all zero.
        margins = box.contentsMargins()
        margins.setLeft(0)
        margins.setRight(0)
        margins.setBottom(0)
        box.setContentsMargins(margins)

        self.setCentralWidget(parent)

    
    def _on_commands_changed(self, index):
        self._update_classification_dict(index)
        
        
    def _update_classification_dict(self, index):
        preset = self._commands_presets[index]
        self._classification_dict = \
            self._create_classification_dict(preset)
        
        
    def _create_classification_dict(self, commands_preset):
        
        """
        Creates a mapping from command names to clip classes for the
        specified commands preset.
        
        The mapping is the composition of the preset's commands dictionary,
        which maps command names to clip class name fragments, and a mapping
        from clip class name fragments to clip classes. For each clip class
        name fragment of a command, there must be exactly one clip class
        whose name ends with that fragment.
        """
        
        commands = commands_preset.commands
        classes = self._archive.clip_classes
        classes_dict = self._create_fragment_to_classes_dict(classes)
        classification_dict = {}
        
        for command_name, (fragment, all_) in commands.iteritems():
            
            classes = classes_dict.get(fragment)
            
            if classes is None:
                # no clip class names include fragment
                
                # TODO: Implement a proper logging facility.
                print((
                    'Warning: Unrecognized clip class name fragment "{}" '
                    'for command "{}" in {} preset "{}". Command will be '
                    'ignored.').format(
                        fragment, command_name, commands_preset.type_name,
                        commands_preset.name),
                    file=sys.stderr)
            
            elif len(classes) != 1:
                # more than one clip class name includes fragment
                
                # Look for clip class whose entire name is fragment.
                clip_class = None
                for c in classes:
                    if c.name == fragment:
                        clip_class = c
                        
                if clip_class is None:
                    # no clip class has name equal to fragment
                    
                    # TODO: Implement a proper logging facility.
                    print((
                        'Warning: Clip class name fragment "{}" for '
                        'command "{}" in {} preset "{}" is ambiguous. '
                        'Command will be ignored.').format(
                            fragment, command_name, commands_preset.type_name,
                            commands_preset.name),
                        file=sys.stderr)
                
                else:
                    # one clip class has name equal to fragment
                    
                    classification_dict[command_name] = (clip_class, all_)
            
            else:
                # exactly one clip class for specified fragment
                
                classification_dict[command_name] = (classes[0], all_)
            
        return classification_dict
        
        
    def _create_fragment_to_classes_dict(self, clip_classes):
        d = defaultdict(list)
        d[Archive.CLIP_CLASS_NAME_UNCLASSIFIED].append(None)
        for c in clip_classes:
            for f in self._get_clip_class_name_fragments(c.name):
                d[f].append(c)
        return d
    
    
    def _get_clip_class_name_fragments(self, name):
        sep = Archive.CLIP_CLASS_NAME_COMPONENT_SEPARATOR
        parts = name.split(sep)
        n = len(parts)
        return [sep.join(parts[i:n]) for i in xrange(n)]
    
    
    def _init_commands(self, preset_name):
        
        if self._commands_combo_box is not None:
            
            index = 0
            
            if preset_name is not None:
                
                try:
                    index = self._commands_indices[preset_name]
                    
                except KeyError:
                    # TODO: Log error.
                    pass
                    
            if self._commands_combo_box.currentIndex() == index:
                self._update_classification_dict(index)
                
            else:
                self._commands_combo_box.setCurrentIndex(index)
                
        else:
            # no commands presets
            
            self._classification_dict = {}
        
        
    def set_clips(self, station_name, detector_name, date, clip_class_name):

        # TODO: Does this make sense? Think about what `None` arguments
        # mean and whether or not we actually support them (it seems like
        # we currently don't, except perhaps for the clip class name).
        if station_name is None or detector_name is None or \
           date is None or clip_class_name is None:
            
            self._clips = []
            
        else:
            # Update clips.
            # TODO: Handle `getClips` failure gracefully.
            # TODO: Get clips on a separate thread so UI thread can
            # keep running. Set clips on self._figures_frame on UI thread
            # when that thread completes.
            print('getting clips from archive...')
            self._clips = self._archive.get_clips(
                station_name, detector_name, date, clip_class_name)
        
        self._station_name = station_name
        self._detector_name = detector_name
        self._date = date
        self._clip_class_name = clip_class_name
        
        print('updating UI...')
        self._update_ui()
        
        print('done')
        
        
    def _update_ui(self):
        frame = self._figures_frame
        frame.clips = self._clips
        self._rug_plot.set_clips(self._clips, frame.page_start_indices)
        self._update_title()
        

    def _update_title(self):
        
        if self._station_name is None or self._detector_name is None or \
           self._date is None or self._clip_class_name is None:
            
            title = 'No Clips'
            
        else:
            title = self._create_clips_string()

        self._rug_plot.current_page_num = self._figures_frame.page_num
        self._title_label.setText(title)

        
    def _create_clips_string(self):
        
        frame = self._figures_frame
        n = len(self._clips)
        
        if n == 0:
            return 'No {:s} Clips'.format(self._clip_class_name)
        
        else:
            
            first = frame.first_visible_clip_num
            last = min(first + frame.num_visible_clips - 1, n - 1)
            
            if first == last:
                return '{:s} Clip {:d} of {:d}'.format(
                    self._clip_class_name, first + 1, n)
            
            else:
                return '{:s} Clips {:d}-{:d} of {:d}'.format(
                    self._clip_class_name, first + 1, last + 1, n)
    
        
    def keyPressEvent(self, e):
        
        command = command_utils.get_command_from_key_event(e)
        
        if command is not None:
            
            pair = self._classification_dict.get(command)
        
            if pair is not None:
                self.classify(*pair)
                
            else:
                # key is not a classification command
                
                self._key_press_event(e)
                
        
        else:
            # key is not a classification command
            
            self._key_press_event(e)
            
            
    def _key_press_event(self, e):
        
        is_key = command_utils.is_key
        
        if is_key(e, Qt.Key_Space) or is_key(e, Qt.Key_PageDown):
            self.move_down_one_page()
            
        elif is_key(e, Qt.Key_Space, Qt.ShiftModifier) or \
                is_key(e, Qt.Key_PageUp):
            
            self.move_up_one_page()
            
        elif is_key(e, Qt.Key_Tab):
            self.move_singleton_selection_forward()
            
        # Note that when the user presses the tab key with the shift
        # key held down, the event we get is for the backtab key
        # rather than the tab key, with the shift modifier.
        elif is_key(e, Qt.Key_Backtab, Qt.ShiftModifier):
            self.move_singleton_selection_backward()
            
        else:
            super(ClipsWindow, self).keyPressEvent(e)

    
    @property
    def classification_dict(self):
        return self._classification_dict
        
        
    def classify(self, clip_class, scope):
        self._figures_frame.classify(clip_class, scope)
        self._update_title()
        

    def move_down_one_page(self):
        self._figures_frame.move_down_one_page()
        self._update_title()
                        
            
    def move_up_one_page(self):
        self._figures_frame.move_up_one_page()
        self._update_title()
        
        
    def move_to_page(self, page_num):
        self._figures_frame.move_to_page(page_num)
        self._update_title()
        
        
    def move_singleton_selection_forward(self):
        self._figures_frame.move_singleton_selection_forward()
        self._update_title()


    def move_singleton_selection_backward(self):
        self._figures_frame.move_singleton_selection_backward()
        self._update_title()


class _FiguresFrame(QWidget):
    
    
    def __init__(self, parent, config):
        
        super(_FiguresFrame, self).__init__(parent)

#        self.setStyleSheet('QFrame { background-color: red }')
        
        self._num_rows = config.num_rows
        self._selection_rect_thickness = config.selection_rect_thickness
        self._selection_rect_color = config.selection_rect_color
        
        self._first_visible_row_num = None
        self._inactive_clip_frames = []
        self._active_clip_frames = []
        
        # We leave the initial setting of `self._clips` and
        # `self._selection` to subclass initializers since that
        # process often uses subclass instance data that have not
        # yet been initialized when this method runs.


    @property
    def num_rows(self):
        return self._num_rows


    @property
    def clips(self):
        return self._clips
    
    
    @clips.setter
    def clips(self, clips):
        # self._show_clip_durs(clips)
        self._set_clips(clips)


    def _show_clip_durs(self, clips):
        sounds = [c.sound for c in clips]
        durs = [len(s.samples) / s.sample_rate for s in sounds]
        durs.sort()
        if len(durs) > 0:
            print('_FigureFrame', durs[0], durs[-1])
            
           
    def _set_clips(self, clips):
        raise NotImplementedError()
    
    
    @property
    def selection(self):
        return self._selection
    
    
    @property
    def total_num_rows(self):
        raise NotImplementedError()
        
        
    @property
    def max_first_visible_row_num(self):
        if self.total_num_rows == 0:
            return None
        else:
            return max(0, self.total_num_rows - self.num_rows)
    
    
    @property
    def first_visible_row_num(self):
        return self._first_visible_row_num
            
        
    @first_visible_row_num.setter
    def first_visible_row_num(self, n):
        if self.total_num_rows != 0:
            self._set_first_visible_row_num(n)
        
        
    def _set_first_visible_row_num(self, n, force_update=False):
        
        n = max(n, 0)
        n = min(n, self.total_num_rows - 1)
        
        if n != self._first_visible_row_num or force_update:
            
            self._first_visible_row_num = n
            
            clip_num = self._get_first_clip_num_of_row(n)
            clips = self._clips[clip_num:clip_num + self.num_visible_clips]
            
            self._activate_clip_frames(clips)
            
    
    @property
    def first_visible_clip_num(self):
        n = self.first_visible_row_num
        return self._get_first_clip_num_of_row(n) if n is not None else None
    
    
    def _get_first_clip_num_of_row(self, n):
        raise NotImplementedError()
    
    
    @property
    def num_visible_clips(self):
        
        if self.total_num_rows == 0:
            return 0
        
        else:
            # have some clips
            
            clip_num = self._get_first_clip_num_of_row(
                self.first_visible_row_num + self.num_rows)
            
            if clip_num is None:
                return len(self._clips) - self.first_visible_clip_num
            
            else:
                return clip_num - self.first_visible_clip_num
    
    
    @property
    def num_pages(self):
        return int(math.ceil(self.total_num_rows / float(self.num_rows)))
    
    
    @property
    def page_start_indices(self):
        return [self._get_first_clip_num_of_row(i * self.num_rows)
                for i in xrange(self.num_pages)]
        
        
    @property
    def page_num(self):
        clip_num = self.first_visible_clip_num
        if clip_num is None:
            return None
        else:
            return np.searchsorted(
                self.page_start_indices, clip_num, side='right') - 1
            
            
    def classify(self, clip_class, scope):
        
        new_name = clip_class.name if clip_class is not None else None
        
        if scope == 'All':
            intervals = ((0, len(self._clips) - 1),)
            self._classify(intervals, new_name)
            
        else:
            
            if self.num_visible_clips == 0:
                return
            
            first = self.first_visible_clip_num
            
            if scope == 'Page':
                intervals = ((first, first + self.num_visible_clips - 1),)
            else:
                intervals = _shift(self.selection.selected_intervals, first)
                
            self._classify(intervals, new_name)
            
            if prefs['clipsWindow.advanceAfterClassification']:
                self._advance_after_classification(scope)
                
                            
    def _classify(self, intervals, new_name):
        
        first = self.first_visible_clip_num
            
        for i, j in intervals:
            
            for k in xrange(i, j + 1):
                
                clip = self._clips[k]
                
                old_name = clip.clip_class_name
                
                if new_name != old_name:
                    
                    clip.clip_class_name = new_name
                    
                    if k >= first and k < first + self.num_visible_clips:
                    
                        # This is very naughty. The figures frame should not
                        # be rooting around in clip frames' private parts and
                        # telling their clip figures to update their clip text.
                        # Instead, clips (as models) should probably support
                        # observers and clip figures (as views of clips) should
                        # update themselves when they are notified that their
                        # clips' classifications have changed.
                        frame = self._active_clip_frames[k - first]
                        figure = frame._clip_figure
                        figure._update_clip_text()
                    

    def _advance_after_classification(self, scope):
        
        if scope == 'Selected':
            self.move_singleton_selection_forward()
                
        elif scope == 'Page':
                    
            last_visible_clip_num = \
                self.first_visible_clip_num + self.num_visible_clips - 1
            
            if last_visible_clip_num != len(self._clips) - 1:
                # there are more clips
                
                self.move_down_one_page()
                
        
    def move_down_one_page(self):
        if self.first_visible_row_num + self.num_rows < self.total_num_rows:
            self._move_by_num_rows(self.num_rows)
        
    
    def _move_by_num_rows(self, num_rows):
        if self.total_num_rows != 0:
            self.first_visible_row_num += num_rows
            
            
    def move_up_one_page(self):
        self._move_by_num_rows(-self.num_rows)
        
    
    def move_to_page(self, page_num):
        if self.total_num_rows != 0:
            if page_num < 0:
                page_num = 0
            elif page_num >= self.num_pages:
                page_num = self.num_pages - 1
            self.first_visible_row_num = page_num * self.num_rows
            
            
    def move_singleton_selection_forward(self):
        
        intervals = self.selection.selected_intervals
        
        if _is_singleton(intervals):
            
            clip_num = self.first_visible_clip_num + intervals[0][0]
            
            last_visible_clip_num = \
                self.first_visible_clip_num + self.num_visible_clips - 1
            
            if clip_num != last_visible_clip_num:
                # selected clip is not last on this page
                
                self._select_clip(clip_num + 1)

            elif clip_num != len(self._clips) - 1:
                # there are clips after this page
                
                self.move_down_one_page()
                self._select_clip(clip_num + 1)
                
                
    def _select_clip(self, clip_num):
        
        self.selection.select(clip_num - self.first_visible_clip_num)
        
        # TODO: Should selections be observable, so that we
        # can observe the selection and update our graphical
        # representation of it when it changes?
        self._update_clip_frame_selection_states()
        

    def _update_clip_frame_selection_states(self):
        for frame in self._active_clip_frames:
            frame.selected = frame.index in self.selection


    def move_singleton_selection_backward(self):

        intervals = self.selection.selected_intervals
        
        if _is_singleton(intervals):
            
            clip_num = self.first_visible_clip_num + intervals[0][0]
            
            if clip_num != self.first_visible_clip_num:
                # selected clip is not first on this page
                
                self._select_clip(clip_num - 1)

            elif clip_num != 0:
                # there are clips before this page
                
                self.move_up_one_page()
                self._select_clip(clip_num - 1)
                
                
    def _handle_selection_event(self, figure_frame, event):
        
        index = figure_frame.index
        selection = self._selection
        
        if _control_down(event):
            selection.toggle(index)
            
        elif _shift_down(event):
            selection.extend(index)
            
        else:
            selection.select(index)
            
        self._update_clip_frame_selection_states()
            
        self.setFocus()


def _shift(intervals, n):
    return tuple([(i + n, j + n) for i, j in intervals])


def _is_singleton(intervals):
    
    if len(intervals) != 1:
        return False
    
    else:
        start, end = intervals[0]
        return start == end
    

class _FiguresFrameWithFlowLayout(_FiguresFrame):
    
    
    def __init__(self, parent, config, **kwargs):
        
        super(_FiguresFrameWithFlowLayout, self).__init__(
            parent, config, **kwargs)
        
        # This helps prevent clip frames for short clips from winding
        # up with zero width, which causes problems on Windows. It is
        # not really a solution to that problem. It can be removed
        # when issue #31 is addressed.
        self.setMinimumSize(500, 300)
        
        # clip layout parameters
        self._clips_area_width = config.clips_area_width
        self._clip_spacing = config.clip_spacing
        self._min_clip_height = config.min_clip_height
        
        self.clips = []
        
        
    def _set_clips(self, clips):
        
        self._clips = clips
        
        self._layout = self._assign_clips_to_rows()

        if self.total_num_rows == 0:
            self._deactivate_clip_frames()
            self._first_visible_row_num = None
            self._selection = None
            
        else:
            self._set_first_visible_row_num(0, force_update=True)
        
            
    def _assign_clips_to_rows(self):
        durations = [clip.duration for clip in self._clips]
        width = self._clips_area_width
        layout = FlowLayout(width, width * self._clip_spacing)
        return layout.lay_out_items(durations)
        
        
    def _deactivate_clip_frames(self):
        
        for frame in self._active_clip_frames:
            frame.hide()
            frame.clip = None
            frame.selected = False
            
        self._inactive_clip_frames += self._active_clip_frames
        self._active_clip_frames = []
        
        
    def _activate_clip_frames(self, clips):
        
        self._deactivate_clip_frames()
        
        m = self._first_visible_row_num
        num_frames = sum(n for (_, n) in self._layout[m:m + self.num_rows])
        
        inactive_frames = self._inactive_clip_frames
        n = num_frames - len(inactive_frames)
        if n > 0:
            print('    creating clip frames...')
            thickness = self._selection_rect_thickness
            color = self._selection_rect_color
            for _ in xrange(n):
                frame = _FigureFrame(self, thickness, color)
                inactive_frames.append(frame)
        
        print('    setting clips on frames...')
        for i in xrange(num_frames):
            frame = inactive_frames.pop()
            frame.clip = clips[i]
            frame.index = i
#            frame.show()
            self._active_clip_frames.append(frame)
        
        print('    creating selection...')
        self._selection = Multiselection(0, len(clips) - 1)
        
        print('    laying out clip frames...')
        self._lay_out_clips()
        
        print('    showing clip frames...')
        self._show_clip_frames()
        
        print('    done')

    
    # This is a Qt event handler, so don't rename it!
    def resizeEvent(self, event):
        self._lay_out_clips()
            
            
    def _lay_out_clips(self):
        
        if len(self._clips) == 0:
            return
        
        size = self.size()
        clips_frame_width = size.width()
        clips_frame_height = size.height()
        
        min_spacing = 2 * self._selection_rect_thickness
        h_spacing = max(clips_frame_width * self._clip_spacing, min_spacing)
        v_spacing = max(
            clips_frame_height * self._clip_spacing * _SPACING_ASPECT_RATIO,
            min_spacing)
        
        (ys, height) = self._lay_out_clip_rows(clips_frame_height, v_spacing)
        
        num_rows = min(
            self.num_rows, self.total_num_rows - self._first_visible_row_num)
        frame_num = 0
        border_sizes = []
                
        for i in xrange(num_rows):
            
            row_num = self._first_visible_row_num + i
            
            (start_clip_num, num_clips) = self._layout[row_num]
            
            # The `max(clips_frame_width, 1)` in the following prevents
            # division by zero.
            seconds_per_pixel = \
                float(self._clips_area_width) / max(clips_frame_width, 1)
            
            clip_widths = [
                int(round(self._clips[j].duration / seconds_per_pixel))
                for j in xrange(start_clip_num, start_clip_num + num_clips)]
            
            # Under this spacing regime, the space between clips is
            # `h_spacing` and the remaining space is divided equally
            # between the left and right borders.
#             spacing = h_spacing
#             borderSize = (clips_frame_width - sum(clip_widths) - \
#                           (num_clips - 1) * spacing) / 2.
#             borderSize = max(borderSize, min_spacing)
            
            # Under this spacing regime, the space in a row is nominally
            # divided evenly between the borders and the spaces between
            # clips. Rows for which this would cause the spacing to be
            # undesirably large are treated specially.
            space = clips_frame_width - sum(clip_widths)
            spacing = space / float(num_clips + 1)
            spacing = max(spacing, min_spacing)
            border_size = spacing
            if spacing >= 5 * h_spacing:
                spacing = 3 * h_spacing
                if row_num != self.total_num_rows - 1:
                    border_size = (space - (num_clips - 1) * spacing) / 2.
                else:
                    sizes = np.array(border_sizes)
                    if len(sizes) == 0:
                        border_size = spacing
                    else:
                        border_size = np.median(sizes)
            border_sizes.append(border_size)

            clip_x = border_size
            y = ys[i]
            
            for i in xrange(num_clips):
                
                x = int(round(clip_x - self._selection_rect_thickness))
                width = clip_widths[i]
                
                frame = self._active_clip_frames[frame_num]
                frame.setGeometry(x, y, width, height)
                
                clip_x += width + spacing
                
                frame_num += 1
                
                
    def _lay_out_clip_rows(self, clips_frame_height, spacing):
        
        min_height = self._min_clip_height
        thickness = self._selection_rect_thickness
        
        total_height = (clips_frame_height - (self.num_rows + 1) * spacing)
        clip_height = max(total_height / float(self.num_rows), min_height)
        clip_frame_height = int(round(clip_height)) + 2 * thickness

        offsets = (clip_height + spacing) * np.arange(self.num_rows)
        ys = spacing - thickness + offsets
        ys = np.array(ys, dtype=int)
        
        return (ys, clip_frame_height)
        

    @property
    def total_num_rows(self):
        return len(self._layout)
    
    
    def _get_first_clip_num_of_row(self, n):
        if n < 0 or n >= self.total_num_rows:
            return None
        else:
            return self._layout[n][0]
        
        
    def _show_clip_frames(self):
        for frame in self._active_clip_frames:
            frame.show()
        
        
class _FigureFrame(QFrame):
    
    """
    Frame that displays one clip.
    
    The frame contains an inset spectrogram and a border whose color
    indicates the selection status of the clip.
    """
    
    
    def __init__(self, parent, selection_rect_thickness, selection_rect_color):
        
        super(_FigureFrame, self).__init__(parent)
        
        self._index = None
        
        self._selection_rect_thickness = selection_rect_thickness
        self._selection_rect_color = selection_rect_color
        
        self._brush = QBrush(QColor(selection_rect_color))
        
        self.selected = False
        
        figure = Figure()
        canvas = FigureCanvasQTAgg(figure)
        canvas.setParent(self)
        self._clip_figure = ClipFigure(self, figure)

        
    @property
    def clip(self):
        return self._clip_figure.clip
    
    
    @clip.setter
    def clip(self, clip):
        self._clip_figure.clip = clip
        
        
    @property
{'fraction_non_alphanumeric': 0.0701536165184402, 'fraction_numerical': 0.0032962248896075625, 'mean_word_length': 1.572371315442147, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '494586', 'n_tokens_mistral': 18283, 'n_tokens_neox': 17510, 'n_words': 4020}