tree.py 9.29 KB
Newer Older
1 2 3
# Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
# Copyright (C) 2011-2013 Kai Willadsen <kai.willadsen@gmail.com>
#
4 5 6 7 8 9 10 11 12 13 14 15
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
steve9000's avatar
steve9000 committed
16 17

import os
18 19 20
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Pango
21

22
COL_PATH, COL_STATE, COL_TEXT, COL_ICON, COL_TINT, COL_FG, COL_STYLE, \
23
    COL_WEIGHT, COL_STRIKE, COL_END = list(range(10))
24

25
COL_TYPES = (str, str, str, str, str, str, Pango.Style,
26
             Pango.Weight, bool)
steve9000's avatar
steve9000 committed
27 28


29 30
from meld.vc._vc import \
    STATE_IGNORED, STATE_NONE, STATE_NORMAL, STATE_NOCHANGE, \
31 32
    STATE_ERROR, STATE_EMPTY, STATE_NEW, \
    STATE_MODIFIED, STATE_CONFLICT, STATE_REMOVED, \
33 34 35
    STATE_MISSING, STATE_NONEXIST, STATE_MAX, \
    CONFLICT_BASE, CONFLICT_LOCAL, CONFLICT_REMOTE, \
    CONFLICT_MERGED, CONFLICT_OTHER, CONFLICT_THIS
steve9000's avatar
steve9000 committed
36 37


38
class DiffTreeStore(Gtk.TreeStore):
39 40

    def __init__(self, ntree, types):
41 42 43
        full_types = []
        for col_type in (COL_TYPES + tuple(types)):
            full_types.extend([col_type] * ntree)
44
        Gtk.TreeStore.__init__(self, *full_types)
steve9000's avatar
steve9000 committed
45
        self.ntree = ntree
46 47
        self._setup_default_styles()

48 49
    def on_style_updated(self, widget):
        style = widget.get_style_context()
50 51 52
        self._setup_default_styles(style)

    def _setup_default_styles(self, style=None):
53 54
        roman, italic = Pango.Style.NORMAL, Pango.Style.ITALIC
        normal, bold = Pango.Weight.NORMAL, Pango.Weight.BOLD
55

56 57 58 59 60 61 62 63 64 65
        def lookup(name, default):
            try:
                found, colour = style.lookup_color(name)
                if found:
                    colour = colour.to_string()
                else:
                    colour = default
            except AttributeError:
                colour = default
            return colour
66 67 68 69 70

        unk_fg = lookup("unknown-text", "#888888")
        new_fg = lookup("insert-text", "#008800")
        mod_fg = lookup("replace-text", "#0044dd")
        del_fg = lookup("delete-text", "#880000")
71
        err_fg = lookup("error-text", "#ffff00")
72 73
        con_fg = lookup("conflict-text", "#ff0000")

74
        self.text_attributes = [
75 76 77 78 79 80 81 82 83 84 85
            # foreground, style, weight, strikethrough
            (unk_fg, roman,  normal, None),  # STATE_IGNORED
            (unk_fg, roman,  normal, None),  # STATE_NONE
            (None,   roman,  normal, None),  # STATE_NORMAL
            (None,   italic, normal, None),  # STATE_NOCHANGE
            (err_fg, roman,  bold,   None),  # STATE_ERROR
            (unk_fg, italic, normal, None),  # STATE_EMPTY
            (new_fg, roman,  bold,   None),  # STATE_NEW
            (mod_fg, roman,  bold,   None),  # STATE_MODIFIED
            (con_fg, roman,  bold,   None),  # STATE_CONFLICT
            (del_fg, roman,  bold,   True),  # STATE_REMOVED
86 87
            (del_fg, roman,  bold,   True),  # STATE_MISSING
            (unk_fg, roman,  normal, True),  # STATE_NONEXIST
88
        ]
89

90 91
        self.icon_details = [
            # file-icon, folder-icon, file-tint, folder-tint
92 93 94 95
            ("text-x-generic", "folder", None,   None),    # IGNORED
            ("text-x-generic", "folder", None,   None),    # NONE
            ("text-x-generic", "folder", None,   None),    # NORMAL
            ("text-x-generic", "folder", None,   None),    # NOCHANGE
96
            ("dialog-warning", None    , None,   None),    # ERROR
97 98 99 100 101 102
            (None,             None    , None,   None),    # EMPTY
            ("text-x-generic", "folder", new_fg, None),    # NEW
            ("text-x-generic", "folder", mod_fg, None),    # MODIFIED
            ("text-x-generic", "folder", con_fg, None),    # CONFLICT
            ("text-x-generic", "folder", del_fg, None),    # REMOVED
            ("text-x-generic", "folder", unk_fg, unk_fg),  # MISSING
103
            ("text-x-generic", "folder", unk_fg, unk_fg),  # NONEXIST
104
        ]
105

106
        assert len(self.icon_details) == len(self.text_attributes) == STATE_MAX
107 108 109 110 111

    def value_paths(self, it):
        return [self.value_path(it, i) for i in range(self.ntree)]

    def value_path(self, it, pane):
112 113 114 115
        path = self.get_value(it, self.column_index(COL_PATH, pane))
        if path is not None:
            path = path.decode('utf8')
        return path
116 117 118

    def column_index(self, col, pane):
        return self.ntree * col + pane
steve9000's avatar
steve9000 committed
119 120 121

    def add_entries(self, parent, names):
        child = self.append(parent)
122 123
        for pane, path in enumerate(names):
            self.set_value(child, self.column_index(COL_PATH, pane), path)
steve9000's avatar
steve9000 committed
124 125 126
        return child

    def add_empty(self, parent, text="empty folder"):
127 128 129 130
        it = self.append(parent)
        for pane in range(self.ntree):
            self.set_value(it, self.column_index(COL_PATH, pane), None)
            self.set_state(it, pane, STATE_EMPTY, text)
steve9000's avatar
steve9000 committed
131 132

    def add_error(self, parent, msg, pane):
133
        it = self.append(parent)
steve9000's avatar
steve9000 committed
134
        for i in range(self.ntree):
135 136 137
            self.set_value(it, self.column_index(COL_STATE, i),
                           str(STATE_ERROR))
        self.set_state(it, pane, STATE_ERROR, msg)
Stephen Kennedy's avatar
Stephen Kennedy committed
138

139
    def set_path_state(self, it, pane, state, isdir=0):
140
        fullname = self.get_value(it, self.column_index(COL_PATH,pane))
141
        name = GObject.markup_escape_text(os.path.basename(fullname))
142 143 144
        self.set_state(it, pane, state, name, isdir)

    def set_state(self, it, pane, state, label, isdir=0):
Kai Willadsen's avatar
Kai Willadsen committed
145 146 147 148 149 150
        col_idx = self.column_index
        icon = self.icon_details[state][1 if isdir else 0]
        tint = self.icon_details[state][3 if isdir else 2]
        self.set_value(it, col_idx(COL_STATE, pane), str(state))
        self.set_value(it, col_idx(COL_TEXT,  pane), label)
        self.set_value(it, col_idx(COL_ICON,  pane), icon)
151 152
        # FIXME: This is horrible, but EmblemCellRenderer crashes
        # if you try to give it a Gdk.Color property
153
        self.set_value(it, col_idx(COL_TINT,  pane), tint)
Kai Willadsen's avatar
Kai Willadsen committed
154 155 156 157 158 159

        fg, style, weight, strike = self.text_attributes[state]
        self.set_value(it, col_idx(COL_FG, pane), fg)
        self.set_value(it, col_idx(COL_STYLE, pane), style)
        self.set_value(it, col_idx(COL_WEIGHT, pane), weight)
        self.set_value(it, col_idx(COL_STRIKE, pane), strike)
160

161
    def get_state(self, it, pane):
steve9000's avatar
steve9000 committed
162
        STATE = self.column_index(COL_STATE, pane)
163 164 165 166
        try:
            return int(self.get_value(it, STATE))
        except TypeError:
            return None
167

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
    def inorder_search_down(self, it):
        while it:
            child = self.iter_children(it)
            if child:
                it = child
            else:
                next = self.iter_next(it)
                if next:
                    it = next
                else:
                    while 1:
                        it = self.iter_parent(it)
                        if it:
                            next = self.iter_next(it)
                            if next:
                                it = next
                                break
                        else:
                            raise StopIteration()
            yield it

    def inorder_search_up(self, it):
        while it:
            path = self.get_path(it)
            if path[-1]:
193
                path = path[:-1] + [path[-1] - 1]
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
                it = self.get_iter(path)
                while 1:
                    nc = self.iter_n_children(it)
                    if nc:
                        it = self.iter_nth_child(it, nc-1)
                    else:
                        break
            else:
                up = self.iter_parent(it)
                if up:
                    it = up
                else:
                    raise StopIteration()
            yield it

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    def _find_next_prev_diff(self, start_path):
        prev_path, next_path = None, None
        start_iter = self.get_iter(start_path)

        for it in self.inorder_search_up(start_iter):
            state = self.get_state(it, 0)
            if state not in (STATE_NORMAL, STATE_EMPTY):
                prev_path = self.get_path(it)
                break

        for it in self.inorder_search_down(start_iter):
            state = self.get_state(it, 0)
            if state not in (STATE_NORMAL, STATE_EMPTY):
                next_path = self.get_path(it)
                break

        return prev_path, next_path
226

227
    def treeview_search_cb(self, model, column, key, it, data):
228 229 230 231 232 233 234 235 236 237 238 239 240 241
        # If the key contains a path separator, search the whole path,
        # otherwise just use the filename. If the key is all lower-case, do a
        # case-insensitive match.
        abs_search = key.find('/') >= 0
        lower_key = key.islower()

        for path in model.value_paths(it):
            if not path:
                continue
            lineText = path if abs_search else os.path.basename(path)
            lineText = lineText.lower() if lower_key else lineText
            if lineText.find(key) != -1:
                return False
        return True