Exemplo n.º 1
0
class DiffView(QWidget):  # {{{

    SYNC_POSITION = 0.4
    line_activated = pyqtSignal(object, object, object)

    def __init__(self, parent=None, show_open_in_editor=False):
        QWidget.__init__(self, parent)
        self.changes = [[], [], []]
        self.delta = 0
        self.l = l = QHBoxLayout(self)
        self.setLayout(l)
        self.syncpos = 0
        l.setContentsMargins(0, 0, 0, 0), l.setSpacing(0)
        self.view = DiffSplit(self, show_open_in_editor=show_open_in_editor)
        l.addWidget(self.view)
        self.add_diff = self.view.add_diff
        self.scrollbar = QScrollBar(self)
        l.addWidget(self.scrollbar)
        self.syncing = False
        self.bars = []
        self.resize_timer = QTimer(self)
        self.resize_timer.setSingleShot(True)
        self.resize_timer.timeout.connect(self.resize_debounced)
        for bar in (self.scrollbar, self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar()):
            self.bars.append(bar)
            bar.scroll_idx = len(self.bars) - 1
            connect_lambda(bar.valueChanged[int], self, lambda self: self.scrolled(self.sender().scroll_idx))
        self.view.left.resized.connect(self.resized)
        for v in (self.view.left, self.view.right, self.view.handle(1)):
            v.wheel_event.connect(self.scrollbar.wheelEvent)
            if v is self.view.left or v is self.view.right:
                v.next_change.connect(self.next_change)
                v.line_activated.connect(self.line_activated)
                connect_lambda(v.scrolled, self,
                        lambda self: self.scrolled(1 if self.sender() is self.view.left else 2))

    def next_change(self, delta):
        assert delta in (1, -1)
        position = self.get_position_from_scrollbar(0)
        if position[0] == 'in':
            p = n = position[1]
        else:
            p, n = position[1], position[1] + 1
            if p < 0:
                p = None
            if n >= len(self.changes[0]):
                n = None
        if p == n:
            nc = p + delta
            if nc < 0 or nc >= len(self.changes[0]):
                nc = None
        else:
            nc = {1:n, -1:p}[delta]
        if nc is None:
            self.scrollbar.setValue(0 if delta == -1 else self.scrollbar.maximum())
        else:
            val = self.scrollbar.value()
            self.scroll_to(0, ('in', nc, 0))
            nval = self.scrollbar.value()
            if nval == val:
                nval += 5 * delta
                if 0 <= nval <= self.scrollbar.maximum():
                    self.scrollbar.setValue(nval)

    def resized(self):
        self.resize_timer.start(300)

    def resize_debounced(self):
        self.view.resized()
        self.calculate_length()
        self.adjust_range()
        self.view.handle(1).update()

    def get_position_from_scrollbar(self, which):
        changes = self.changes[which]
        bar = self.bars[which]
        syncpos = self.syncpos + bar.value()
        prev = 0
        for i, (top, bot, kind) in enumerate(changes):
            if syncpos <= bot:
                if top <= syncpos:
                    # syncpos is inside a change
                    try:
                        ratio = float(syncpos - top) / (bot - top)
                    except ZeroDivisionError:
                        ratio = 0
                    return 'in', i, ratio
                else:
                    # syncpos is after the previous change
                    offset = syncpos - prev
                    return 'after', i - 1, offset
            else:
                # syncpos is after the current change
                prev = bot
        offset = syncpos - prev
        return 'after', len(changes) - 1, offset

    def scroll_to(self, which, position):
        changes = self.changes[which]
        bar = self.bars[which]
        val = None
        if position[0] == 'in':
            change_idx, ratio = position[1:]
            start, end = changes[change_idx][:2]
            val = start + int((end - start) * ratio)
        else:
            change_idx, offset = position[1:]
            start = 0 if change_idx < 0 else changes[change_idx][1]
            val = start + offset
        bar.setValue(val - self.syncpos)

    def scrolled(self, which, *args):
        if self.syncing:
            return
        position = self.get_position_from_scrollbar(which)
        with self:
            for x in {0, 1, 2} - {which}:
                self.scroll_to(x, position)
        self.view.handle(1).update()

    def __enter__(self):
        self.syncing = True

    def __exit__(self, *args):
        self.syncing = False

    def clear(self):
        with self:
            self.view.clear()
            self.changes = [[], [], []]
            self.delta = 0
            self.scrollbar.setRange(0, 0)

    def adjust_range(self):
        ls, rs = self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar()
        self.scrollbar.setPageStep(min(ls.pageStep(), rs.pageStep()))
        self.scrollbar.setSingleStep(min(ls.singleStep(), rs.singleStep()))
        self.scrollbar.setRange(0, ls.maximum() + self.delta)
        self.scrollbar.setVisible(self.view.left.document().lineCount() > ls.pageStep() or self.view.right.document().lineCount() > rs.pageStep())
        self.syncpos = int(ceil(self.scrollbar.pageStep() * self.SYNC_POSITION))

    def finalize(self):
        self.view.finalize()
        self.changes = [[], [], []]
        self.calculate_length()
        self.adjust_range()

    def calculate_length(self):
        delta = 0
        line_number_changes = ([], [])
        for v, lmap, changes in zip((self.view.left, self.view.right), ({}, {}), line_number_changes):
            b = v.document().firstBlock()
            ebl = v.document().documentLayout().ensureBlockLayout
            last_line_count = 0
            while b.isValid():
                ebl(b)
                lmap[b.blockNumber()] = last_line_count
                last_line_count += b.layout().lineCount()
                b = b.next()
            for top, bot, kind in v.changes:
                changes.append((lmap[top], lmap[bot], kind))

        changes = []
        for (l_top, l_bot, kind), (r_top, r_bot, kind) in zip(*line_number_changes):
            height = max(l_bot - l_top, r_bot - r_top)
            top = delta + l_top
            changes.append((top, top + height, kind))
            delta = top + height - l_bot
        self.changes, self.delta = (changes,) + line_number_changes, delta

    def handle_key(self, ev):
        amount, d = None, 1
        key = ev.key()
        if key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_J, Qt.Key_K):
            amount = self.scrollbar.singleStep()
            if key in (Qt.Key_Up, Qt.Key_K):
                d = -1
        elif key in (Qt.Key_PageUp, Qt.Key_PageDown):
            amount = self.scrollbar.pageStep()
            if key in (Qt.Key_PageUp,):
                d = -1
        elif key in (Qt.Key_Home, Qt.Key_End):
            self.scrollbar.setValue(0 if key == Qt.Key_Home else self.scrollbar.maximum())
            return True
        elif key in (Qt.Key_N, Qt.Key_P):
            self.next_change(1 if key == Qt.Key_N else -1)
            return True

        if amount is not None:
            self.scrollbar.setValue(self.scrollbar.value() + d * amount)
            return True
        return False
Exemplo n.º 2
0
class DiffView(QWidget):  # {{{

    SYNC_POSITION = 0.4
    line_activated = pyqtSignal(object, object, object)

    def __init__(self, parent=None, show_open_in_editor=False):
        QWidget.__init__(self, parent)
        self.changes = [[], [], []]
        self.delta = 0
        self.l = l = QHBoxLayout(self)
        self.setLayout(l)
        self.syncpos = 0
        l.setContentsMargins(0, 0, 0, 0), l.setSpacing(0)
        self.view = DiffSplit(self, show_open_in_editor=show_open_in_editor)
        l.addWidget(self.view)
        self.add_diff = self.view.add_diff
        self.scrollbar = QScrollBar(self)
        l.addWidget(self.scrollbar)
        self.syncing = False
        self.bars = []
        self.resize_timer = QTimer(self)
        self.resize_timer.setSingleShot(True)
        self.resize_timer.timeout.connect(self.resize_debounced)
        for i, bar in enumerate((self.scrollbar, self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar())):
            self.bars.append(bar)
            bar.valueChanged[int].connect(partial(self.scrolled, i))
        self.view.left.resized.connect(self.resized)
        for i, v in enumerate((self.view.left, self.view.right, self.view.handle(1))):
            v.wheel_event.connect(self.scrollbar.wheelEvent)
            if i < 2:
                v.next_change.connect(self.next_change)
                v.line_activated.connect(self.line_activated)
                v.scrolled.connect(partial(self.scrolled, i + 1))

    def next_change(self, delta):
        assert delta in (1, -1)
        position = self.get_position_from_scrollbar(0)
        if position[0] == 'in':
            p = n = position[1]
        else:
            p, n = position[1], position[1] + 1
            if p < 0:
                p = None
            if n >= len(self.changes[0]):
                n = None
        if p == n:
            nc = p + delta
            if nc < 0 or nc >= len(self.changes[0]):
                nc = None
        else:
            nc = {1:n, -1:p}[delta]
        if nc is None:
            self.scrollbar.setValue(0 if delta == -1 else self.scrollbar.maximum())
        else:
            val = self.scrollbar.value()
            self.scroll_to(0, ('in', nc, 0))
            nval = self.scrollbar.value()
            if nval == val:
                nval += 5 * delta
                if 0 <= nval <= self.scrollbar.maximum():
                    self.scrollbar.setValue(nval)

    def resized(self):
        self.resize_timer.start(300)

    def resize_debounced(self):
        self.view.resized()
        self.calculate_length()
        self.adjust_range()
        self.view.handle(1).update()

    def get_position_from_scrollbar(self, which):
        changes = self.changes[which]
        bar = self.bars[which]
        syncpos = self.syncpos + bar.value()
        prev = 0
        for i, (top, bot, kind) in enumerate(changes):
            if syncpos <= bot:
                if top <= syncpos:
                    # syncpos is inside a change
                    try:
                        ratio = float(syncpos - top) / (bot - top)
                    except ZeroDivisionError:
                        ratio = 0
                    return 'in', i, ratio
                else:
                    # syncpos is after the previous change
                    offset = syncpos - prev
                    return 'after', i - 1, offset
            else:
                # syncpos is after the current change
                prev = bot
        offset = syncpos - prev
        return 'after', len(changes) - 1, offset

    def scroll_to(self, which, position):
        changes = self.changes[which]
        bar = self.bars[which]
        val = None
        if position[0] == 'in':
            change_idx, ratio = position[1:]
            start, end = changes[change_idx][:2]
            val = start + int((end - start) * ratio)
        else:
            change_idx, offset = position[1:]
            start = 0 if change_idx < 0 else changes[change_idx][1]
            val = start + offset
        bar.setValue(val - self.syncpos)

    def scrolled(self, which, *args):
        if self.syncing:
            return
        position = self.get_position_from_scrollbar(which)
        with self:
            for x in {0, 1, 2} - {which}:
                self.scroll_to(x, position)
        self.view.handle(1).update()

    def __enter__(self):
        self.syncing = True

    def __exit__(self, *args):
        self.syncing = False

    def clear(self):
        with self:
            self.view.clear()
            self.changes = [[], [], []]
            self.delta = 0
            self.scrollbar.setRange(0, 0)

    def adjust_range(self):
        ls, rs = self.view.left.verticalScrollBar(), self.view.right.verticalScrollBar()
        self.scrollbar.setPageStep(min(ls.pageStep(), rs.pageStep()))
        self.scrollbar.setSingleStep(min(ls.singleStep(), rs.singleStep()))
        self.scrollbar.setRange(0, ls.maximum() + self.delta)
        self.scrollbar.setVisible(self.view.left.blockCount() > ls.pageStep() or self.view.right.blockCount() > rs.pageStep())
        self.syncpos = int(ceil(self.scrollbar.pageStep() * self.SYNC_POSITION))

    def finalize(self):
        self.view.finalize()
        self.changes = [[], [], []]
        self.calculate_length()
        self.adjust_range()

    def calculate_length(self):
        delta = 0
        line_number_changes = ([], [])
        for v, lmap, changes in zip((self.view.left, self.view.right), ({}, {}), line_number_changes):
            b = v.document().firstBlock()
            ebl = v.document().documentLayout().ensureBlockLayout
            last_line_count = 0
            while b.isValid():
                ebl(b)
                lmap[b.blockNumber()] = last_line_count
                last_line_count += b.layout().lineCount()
                b = b.next()
            for top, bot, kind in v.changes:
                changes.append((lmap[top], lmap[bot], kind))

        changes = []
        for (l_top, l_bot, kind), (r_top, r_bot, kind) in zip(*line_number_changes):
            height = max(l_bot - l_top, r_bot - r_top)
            top = delta + l_top
            changes.append((top, top + height, kind))
            delta = top + height - l_bot
        self.changes, self.delta = (changes,) + line_number_changes, delta

    def handle_key(self, ev):
        amount, d = None, 1
        key = ev.key()
        if key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_J, Qt.Key_K):
            amount = self.scrollbar.singleStep()
            if key in (Qt.Key_Up, Qt.Key_K):
                d = -1
        elif key in (Qt.Key_PageUp, Qt.Key_PageDown):
            amount = self.scrollbar.pageStep()
            if key in (Qt.Key_PageUp,):
                d = -1
        elif key in (Qt.Key_Home, Qt.Key_End):
            self.scrollbar.setValue(0 if key == Qt.Key_Home else self.scrollbar.maximum())
            return True
        elif key in (Qt.Key_N, Qt.Key_P):
            self.next_change(1 if key == Qt.Key_N else -1)
            return True

        if amount is not None:
            self.scrollbar.setValue(self.scrollbar.value() + d * amount)
            return True
        return False