tree.py 8.28 KB
Newer Older
1
# Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
2
# Copyright (C) 2011-2015 Kai Willadsen <kai.willadsen@gmail.com>
3
#
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
from gi.repository import Gdk
20
from gi.repository import GLib
21
from gi.repository import Pango
22

23
from meld.misc import colour_lookup_with_fallback
24
from meld.treehelpers import SearchableTreeStore
25
from meld.vc._vc import (  # noqa: F401
26 27 28 29
    CONFLICT_BASE, CONFLICT_LOCAL, CONFLICT_MERGED, CONFLICT_OTHER,
    CONFLICT_REMOTE, CONFLICT_THIS, STATE_CONFLICT, STATE_EMPTY, STATE_ERROR,
    STATE_IGNORED, STATE_MAX, STATE_MISSING, STATE_MODIFIED, STATE_NEW,
    STATE_NOCHANGE, STATE_NONE, STATE_NONEXIST, STATE_NORMAL, STATE_REMOVED,
30
)
steve9000's avatar
steve9000 committed
31

32 33 34 35 36 37
COL_PATH, COL_STATE, COL_TEXT, COL_ICON, COL_TINT, COL_FG, COL_STYLE, \
    COL_WEIGHT, COL_STRIKE, COL_END = list(range(10))

COL_TYPES = (str, str, str, str, str, Gdk.RGBA, Pango.Style,
             Pango.Weight, bool)

steve9000's avatar
steve9000 committed
38

39
class DiffTreeStore(SearchableTreeStore):
40 41

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

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

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

57 58 59 60 61 62 63
        lookup = colour_lookup_with_fallback
        unk_fg = lookup("meld:unknown-text", "foreground")
        new_fg = lookup("meld:insert", "foreground")
        mod_fg = lookup("meld:replace", "foreground")
        del_fg = lookup("meld:delete", "foreground")
        err_fg = lookup("meld:error", "foreground")
        con_fg = lookup("meld:conflict", "foreground")
64

65
        self.text_attributes = [
66 67 68 69 70 71 72 73 74
            # 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
75
            (mod_fg, roman,  normal, None),  # STATE_RENAMED
76 77
            (con_fg, roman,  bold,   None),  # STATE_CONFLICT
            (del_fg, roman,  bold,   True),  # STATE_REMOVED
78 79
            (del_fg, roman,  bold,   True),  # STATE_MISSING
            (unk_fg, roman,  normal, True),  # STATE_NONEXIST
80
        ]
81

82 83
        self.icon_details = [
            # file-icon, folder-icon, file-tint, folder-tint
84 85 86 87
            ("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
Kai Willadsen's avatar
Kai Willadsen committed
88 89
            ("dialog-warning", None,     None,   None),    # ERROR
            (None,             None,     None,   None),    # EMPTY
90 91
            ("text-x-generic", "folder", new_fg, None),    # NEW
            ("text-x-generic", "folder", mod_fg, None),    # MODIFIED
92
            ("text-x-generic", "folder", mod_fg, None),    # RENAMED
93 94 95
            ("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
96
            ("text-x-generic", "folder", unk_fg, unk_fg),  # NONEXIST
97
        ]
98

99
        assert len(self.icon_details) == len(self.text_attributes) == STATE_MAX
100 101 102 103 104

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

    def value_path(self, it, pane):
105
        return self.get_value(it, self.column_index(COL_PATH, pane))
106

107 108 109 110 111 112
    def is_folder(self, it, pane, path):
        # A folder may no longer exist, and is only tracked by VC.
        # Therefore, check the icon instead, as the pane already knows.
        icon = self.get_value(it, self.column_index(COL_ICON, pane))
        return icon == "folder" or os.path.isdir(path)

113 114
    def column_index(self, col, pane):
        return self.ntree * col + pane
steve9000's avatar
steve9000 committed
115 116 117

    def add_entries(self, parent, names):
        child = self.append(parent)
118 119
        for pane, path in enumerate(names):
            self.set_value(child, self.column_index(COL_PATH, pane), path)
steve9000's avatar
steve9000 committed
120 121 122
        return child

    def add_empty(self, parent, text="empty folder"):
123 124 125 126
        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
127 128

    def add_error(self, parent, msg, pane):
129
        it = self.append(parent)
steve9000's avatar
steve9000 committed
130
        for i in range(self.ntree):
131 132 133
            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
134

135 136 137 138 139
    def set_path_state(self, it, pane, state, isdir=0, display_text=None):
        if not display_text:
            fullname = self.get_value(it, self.column_index(COL_PATH, pane))
            display_text = GLib.markup_escape_text(os.path.basename(fullname))
        self.set_state(it, pane, state, display_text, isdir)
140 141

    def set_state(self, it, pane, state, label, isdir=0):
Kai Willadsen's avatar
Kai Willadsen committed
142 143 144 145 146 147
        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)
148 149
        # FIXME: This is horrible, but EmblemCellRenderer crashes
        # if you try to give it a Gdk.Color property
150 151
        if tint:
            tint = tint.to_string() if tint else None
152
        self.set_value(it, col_idx(COL_TINT, pane), tint)
Kai Willadsen's avatar
Kai Willadsen committed
153 154 155 156 157 158

        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)
159

160
    def get_state(self, it, pane):
161
        state_idx = self.column_index(COL_STATE, pane)
162
        try:
163
            return int(self.get_value(it, state_idx))
164 165
        except TypeError:
            return None
166

167
    def _find_next_prev_diff(self, start_path):
168 169 170
        def match_func(it):
            # TODO: It works, but matching on the first pane only is very poor
            return self.get_state(it, 0) not in (STATE_NORMAL, STATE_EMPTY)
171

172
        return self.get_previous_next_paths(start_path, match_func)
173

174 175 176 177 178 179 180 181 182 183
    def state_rows(self, states):
        """Generator of rows in one of the given states

        Tree iterators are returned in depth-first tree order.
        """
        root = self.get_iter_first()
        for it in self.inorder_search_down(root):
            state = self.get_state(it, 0)
            if state in states:
                yield it
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200


def treeview_search_cb(model, column, key, it, data):
    # 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 = '/' in key
    lower_key = key.islower()

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