Example #1
0
class MatrixMoveTest(unittest.TestCase):

    def setUp(self):
        data = [
            ['One', 'Two', 'Three', 'Four'],
            ['Aye', 'Bee', 'See', 'Door'],
            ['Bip', 'Blop', 'Foo', 'Bar'],
            ['Cats', 'Dogs', 'Yay', 'Oops']
        ]
        self.matrix = Matrix(default_item, data)

    def test_move_row_down(self):
        data = [
            ['Aye', 'Bee', 'See', 'Door'],
            ['Bip', 'Blop', 'Foo', 'Bar'],
            ['One', 'Two', 'Three', 'Four'],
            ['Cats', 'Dogs', 'Yay', 'Oops']
        ]
        self.matrix.move_row(0,2)
        self.assertEqual(self.matrix.data, data)

    def test_move_row_up(self):
        data = [
            ['One', 'Two', 'Three', 'Four'],
            ['Aye', 'Bee', 'See', 'Door'],
            ['Cats', 'Dogs', 'Yay', 'Oops'],
            ['Bip', 'Blop', 'Foo', 'Bar']
        ]
        self.matrix.move_row(3,2)
        self.assertEqual(self.matrix.data, data)

    def test_move_row_last_to_first(self):
        data = [
            ['Cats', 'Dogs', 'Yay', 'Oops'],
            ['One', 'Two', 'Three', 'Four'],
            ['Aye', 'Bee', 'See', 'Door'],
            ['Bip', 'Blop', 'Foo', 'Bar']
        ]
        self.matrix.move_row(3,0)
        self.assertEqual(self.matrix.data, data)

    def test_move_row_first_to_last(self):
        data = [
            ['Aye', 'Bee', 'See', 'Door'],
            ['Bip', 'Blop', 'Foo', 'Bar'],
            ['Cats', 'Dogs', 'Yay', 'Oops'],
            ['One', 'Two', 'Three', 'Four']
        ]
        self.matrix.move_row(0,3)
        self.assertEqual(self.matrix.data, data)

    def test_move_col_right(self):
        data = [
            ['One', 'Three', 'Four', 'Two'],
            ['Aye', 'See', 'Door', 'Bee'],
            ['Bip', 'Foo', 'Bar', 'Blop'],
            ['Cats', 'Yay', 'Oops', 'Dogs']
        ]
        self.matrix.move_col(1,3)
        self.assertEqual(self.matrix.data, data)

    def test_move_col_left(self):
        data = [
            ['One', 'Three', 'Two', 'Four'],
            ['Aye', 'See', 'Bee', 'Door'],
            ['Bip', 'Foo', 'Blop', 'Bar'],
            ['Cats', 'Yay', 'Dogs', 'Oops']
        ]
        self.matrix.move_col(2,1)
        self.assertEqual(self.matrix.data, data)

    def test_move_col_last_to_first(self):
        data = [
            ['Four', 'One', 'Two', 'Three'],
            ['Door', 'Aye', 'Bee', 'See'],
            ['Bar', 'Bip', 'Blop', 'Foo'],
            ['Oops', 'Cats', 'Dogs', 'Yay']
        ]
        self.matrix.move_col(3,0)
        self.assertEqual(self.matrix.data, data)

    def test_move_col_first_to_last(self):
        data = [
            ['Two', 'Three', 'Four', 'One'],
            ['Bee', 'See', 'Door', 'Aye'],
            ['Blop', 'Foo', 'Bar', 'Bip'],
            ['Dogs', 'Yay', 'Oops', 'Cats']
        ]
        self.matrix.move_col(0,3)
        self.assertEqual(self.matrix.data, data)
Example #2
0
File: scene.py Project: nycz/urd
class Scene(QtGui.QWidget, FileHandler):
    print_ = pyqtSignal(str)
    error_sig = pyqtSignal(str)
    prompt_sig = pyqtSignal(str)
    window_title_changed = pyqtSignal(str)

    def __init__(self, config, get_hsbar_pos, get_vsbar_pos):
        super().__init__()
        self.theme = config['theme']

        self.get_hsbar_pos = get_hsbar_pos
        self.get_vsbar_pos = get_vsbar_pos

        self.horizontal_time = False
        self.modified_flag = False
        self.file_path = ''

        self.grid = Matrix()
        self.undo_stack = []

        font = QtGui.QFont(self.theme['font'], self.theme['font size'])
        boldfont = QtGui.QFont(self.theme['font'], self.theme['font size'],
                               weight=QtGui.QFont.Bold)
        self.font_data = {'def': (font, QtGui.QFontMetrics(font)),
                          'bold': (boldfont, QtGui.QFontMetrics(boldfont))}

        self.draw_scene()


    def paintEvent(self, ev):
        painter = QtGui.QPainter(self)
        painter.drawPixmap(0, 0, self.scene_image)
        rx, ry = self.get_hsbar_pos(), self.get_vsbar_pos()
        painter.drawPixmap(0, ry, self.header_row)
        painter.drawPixmap(rx, 0, self.header_col)
        painter.drawPixmap(rx, ry, self.corner)

    def draw_scene(self):
        border = self.theme['border']
        row_padding, col_padding = 20, 20
        row_heights = [max([x[1][1] for x in self.grid.row(row) if x[0]] + [0]) + row_padding
                       for row in range(self.grid.count_rows())]
        col_widths = [max([x[1][0] for x in self.grid.col(col) if x[0]] + [0]) + col_padding
                      for col in range(self.grid.count_cols())]

        width = sum(col_widths) + border
        height = sum(row_heights) + border
        self.scene_image = QtGui.QPixmap(width, height)

        # Begin paint
        self.scene_image.fill(QColor(self.theme['background']))
        painter = QtGui.QPainter(self.scene_image)
        painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing)
        painter.setFont(self.font_data['def'][0])
        painter.setBrush(QColor(self.theme['cell background']))

        # Draw header lines
        hy = int(border + row_heights[0])
        vx = int(border + col_widths[0])
        painter.setPen(QColor(self.theme['details']))
        painter.drawLine(0, hy, border + sum(col_widths), hy)
        painter.drawLine(vx, 0, vx, border + sum(row_heights))

        # Draw border numbers #TODO: fix the offsets
        for n, rh in list(enumerate(row_heights))[1:]:
            painter.drawText(4, border + sum(row_heights[:n]) + rh/2 + 5, str(n))
        for n, cw in list(enumerate(col_widths))[1:]:
            painter.drawText(border + sum(col_widths[:n]) + cw/2 - 5, 16, str(n))

        # Draw plotline lines
        for n, size in enumerate((col_widths, row_heights)[self.horizontal_time]):
            if n == 0: continue
            if self.horizontal_time:
                x, y = col_widths[0]/2, sum(row_heights[:n]) + size/2 - 5
                w, h = sum(col_widths), 10
            else:
                x, y = sum(col_widths[:n]) + size/2 - 5, row_heights[0]/2
                w, h = 10, sum(row_heights)
            painter.fillRect(border + x, border + y, w, h, QColor('black'))

        # Draw cells
        painter.setPen(QColor(self.theme['cell border']))
        for r in range(self.grid.count_rows()):
            for c in range(self.grid.count_cols()):
                text, size = self.grid[c,r]
                if not text: continue
                if c == 0 or r == 0:
                    painter.setFont(self.font_data['bold'][0])
                else:
                    painter.setFont(self.font_data['def'][0])
                x = border + sum(col_widths[:c]) + col_widths[c]/2 - size[0]/2
                y = border + sum(row_heights[:r]) + row_heights[r]/2 - size[1]/2
                painter.drawRoundedRect(x-3, y-3, size[0]+6, size[1]+6, 5, 5)
                painter.drawText(x, y, size[0], size[1], Qt.TextWordWrap, text)

        painter.end()

        hw = border + col_widths[0]
        hh = border + row_heights[0]

        self.header_row = self.scene_image.copy(0,0, width, hh)
        self.header_col = self.scene_image.copy(0,0, hw, height)

        self.corner = self.scene_image.copy(0,0, hw, hh)

        self.resize(width, height)
        self.update()


    def prompt(self, text):
        self.prompt_sig.emit(text)

    def error(self, text):
        self.error_sig.emit(text)


    def do_row(self, arg):
        return (self.horizontal_time and arg == 'p') \
            or (not self.horizontal_time and arg == 't')

    def set_time_orientation(self, orientation):
        if orientation not in 'hv':
            self.error('Orientation must be h or v')
            return
        if (orientation == 'h' and self.horizontal_time) or (orientation == 'v' and not self.horizontal_time):
            return
        self.add_undo('flip', None)
        self.grid.flip_orientation()
        self.horizontal_time = not self.horizontal_time
        self.draw_scene()


    # ======== ADD ===========================================================
    def add_plotline(self, name):
        self.insert_plotline(-1, name)

    def add_timeslot(self, name):
        self.insert_timeslot(-1, name)

    # ======== INSERT ========================================================
    def insert_plotline(self, pos, name):
        self._insert(pos, name, 'p')

    def insert_timeslot(self, pos, name):
        self._insert(pos, name, 't')

    def _insert(self, pos, name, arg):
        if self.do_row(arg):
            if pos != -1 and (0,pos-1) not in self.grid:
                self.error('Invalid row')
                return
            self.grid.add_row(pos)
            x, y = 0, pos
            self.add_undo('ir', pos)
        else:
            if pos != -1 and (pos-1,0) not in self.grid:
                self.error('Invalid column')
                return
            self.grid.add_col(pos)
            x, y = pos, 0
            self.add_undo('ic', pos)
        self.set_cell(x, y, name)
        self.draw_scene()


    # ======== MOVE ==========================================================
    def move_plotline(self, oldpos, newpos):
        self._move(oldpos, newpos, 'p')

    def move_timeslot(self, oldpos, newpos):
        self._move(oldpos, newpos, 't')

    def _move(self, oldpos, newpos, arg):
        foldpos, fnewpos = _fix_movepos(oldpos, newpos)
        if self.do_row(arg):
            if (0,oldpos) not in self.grid:
                self.error('Invalid row')
                return
            self.add_undo('mr', (oldpos, newpos))
            self.grid.move_row(foldpos, fnewpos)
        else:
            if (oldpos,0) not in self.grid:
                self.error('Invalid column')
                return
            self.add_undo('mc', (oldpos, newpos))
            self.grid.move_col(foldpos, fnewpos)
        self.draw_scene()

    # ======== REMOVE ========================================================
    def remove_plotline(self, pos):
        self._remove(pos, 'p')

    def remove_timeslot(self, pos):
        self._remove(pos, 't')

    def _remove(self, pos, arg):
        if self.do_row(arg):
            if (0,pos) not in self.grid:
                self.error('Invalid row')
                return
            self.add_undo('rr', (pos, self.grid.row(pos)))
            self.grid.remove_row(pos)
        else:
            if (pos,0) not in self.grid:
                self.error('Invalid column')
                return
            self.add_undo('rc', (pos, self.grid.col(pos)))
            self.grid.remove_col(pos)
        self.draw_scene()

    # ======== CELLS =========================================================
    def move_cell(self, x1, y1, x2, y2):
        if not ((x1,y1) in self.grid and (x2,y2) in self.grid):
            self.error('Invalid coordinates')
            return
        self.add_undo('mi', ((x1, y1, self.grid[x1,y1]), (x2, y2, self.grid[x2,y2])))
        self.grid[x2,y2] = self.grid[x1,y1]
        self.grid[x1,y1] = None
        self.draw_scene()

    def edit_cell(self, x, y, text):
        if (x,y) not in self.grid:
            self.error('Invalid coordinate')
            return
        if not text:
            self.prompt('e{} {} {}'.format(x, y, self.grid[x,y][0]))
            return
        self.add_undo('e', (x, y, self.grid[x,y]))
        self.set_cell(x, y, text)
        self.draw_scene()

    def clear_cell(self, x, y):
        if (x,y) not in self.grid:
            self.error('Invalid coordinate')
            return
        self.add_undo('e', (x, y, self.grid[x,y]))
        self.grid[x,y] = None
        self.draw_scene()

    def set_cell(self, x, y, name):
        if x == 0 and y == 0:
            self.error('Can\'t edit cell 0,0!')
            return
        if x == 0 or y == 0:
            fd = 'bold'
        else:
            fd = 'def'
        name = name.replace('\\n','\n')
        size = self.font_data[fd][1].boundingRect(0,0,150,10000,Qt.TextWordWrap,name).size()
        self.grid[x,y] = (name, (size.width(), size.height()))

    # ======== UNDO ==========================================================
    def add_undo(self, cmd, arg):
        self.modified_flag = True
        self.update_title()
        self.undo_stack.append((cmd, arg))

    def undo(self):
        if not self.undo_stack:
            self.error('Nothing to undo')
            return
        cmd, arg = self.undo_stack.pop()
        if cmd == 'e':
            x, y, item = arg
            self.grid[x,y] = item
        elif cmd[0] == 'm':
            if cmd[1] == 'r':
                old, new = _fix_movepos(*arg)
                self.grid.move_row(new, old)
            elif cmd[1] == 'c':
                old, new = _fix_movepos(*arg)
                self.grid.move_col(new, old)
            elif cmd[1] == 'i':
                oldcell, newcell = arg
                x1, y1, item1 = oldcell
                x2, y2, item2 = newcell
                self.grid[x1,y1] = item1
                self.grid[x2,y2] = item2
        elif cmd[0] == 'i':
            if cmd[1] == 'r':
                self.grid.remove_row(arg)
            else:
                self.grid.remove_col(arg)
        elif cmd[0] == 'r':
            pos, arr = arg
            if cmd[1] == 'r':
                self.grid.add_row(pos)
                for n, item in enumerate(arr):
                    self.grid[n,pos] = item
            else:
                self.grid.add_col(pos)
                for n, item in enumerate(arr):
                    self.grid[pos,n] = item
        elif cmd == 'flip':
            self.grid.flip_orientation()
            self.horizontal_time = not self.horizontal_time
        else:
            self.error('Unknown undo: ' + cmd)
            return
        self.draw_scene()

    # ======= FILE NAME =================================================
    def update_title(self):
        title = '{0}{1}{0}'.format('*'*self.modified_flag, self.file_path or 'New file')
        self.window_title_changed.emit(title)

    def set_filename(self, filename=''):
        self.file_path = filename
        self.update_title()

    # ======= FILE HANDLING =============================================
    def is_modified(self):
        return self.modified_flag

    def dirty_window_and_start_in_new_process(self):
        return False #TODO

    def post_new(self):
        self.undo_stack = []
        self.grid.clear()
        self.draw_scene()
        self.modified_flag = False
        self.set_filename()

    def open_file(self, filename):
        text_matrix = []
        with open(filename, newline='', encoding='utf-8') as f:
            reader = csv.reader(f, dialect='unix', quoting=csv.QUOTE_MINIMAL)
            for row in reader:
                text_matrix.append(row)

        if text_matrix[0][0] in ('HORIZONTAL TIME', 'VERTICAL TIME'):
            if text_matrix[0][0] == ['HORIZONTAL TIME']:
                self.horizontal_time = True
            elif text_matrix[0][0] == ['VERTICAL TIME']:
                self.horizontal_time = False
            text_matrix[0][0] = ''

        self.grid.clear()
        for _ in range(len(text_matrix)-1):
            self.grid.add_row()
        for _ in range(len(text_matrix[0])-1):
            self.grid.add_col()
        for rown, row in enumerate(text_matrix):
            for coln, text in enumerate(row):
                if coln == 0 and rown == 0:
                    continue
                self.set_cell(coln, rown, text)
        self.draw_scene()

        self.modified_flag = False
        self.set_filename(filename)
        self.undo_stack = []
        return True

    def write_file(self, filename):
        direction = 'HORIZONTAL TIME' if self.horizontal_time else 'VERTICAL TIME'
        first_row = [direction] + [x[0] for x in self.grid.row(0)[1:]]
        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f, dialect='unix', quoting=csv.QUOTE_MINIMAL)
            writer.writerow(first_row)
            for row in list(range(self.grid.count_rows()))[1:]:
                writer.writerow([x[0] for x in self.grid.row(row)])

    def post_save(self, saved_filename):
        self.modified_flag = False
        self.set_filename(saved_filename)