tree.py 10.8 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.module import get_introspection_module
20
from gi.repository import Gdk
21
from gi.repository import GLib
22
from gi.repository import GObject
23
from gi.repository import Pango
24

25
from meld.misc import colour_lookup_with_fallback
26
from meld.treehelpers import SearchableTreeStore
27
from meld.vc._vc import (  # noqa: F401
28 29 30 31
    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,
32
)
steve9000's avatar
steve9000 committed
33

34 35 36 37 38 39 40
_GIGtk = None

try:
    _GIGtk = get_introspection_module('Gtk')
except Exception:
    pass

41 42 43
COL_PATH, COL_STATE, COL_TEXT, COL_ICON, COL_TINT, COL_FG, COL_STYLE, \
    COL_WEIGHT, COL_STRIKE, COL_END = list(range(10))

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

steve9000's avatar
steve9000 committed
47

48
class DiffTreeStore(SearchableTreeStore):
49 50

    def __init__(self, ntree, types):
51 52 53
        full_types = []
        for col_type in (COL_TYPES + tuple(types)):
            full_types.extend([col_type] * ntree)
54
        super().__init__(*full_types)
55 56 57 58
        self._none_of_cols = {
            col_num: GObject.Value(col_type, None)
            for col_num, col_type in enumerate(full_types)
        }
steve9000's avatar
steve9000 committed
59
        self.ntree = ntree
60 61
        self._setup_default_styles()

62 63
    def on_style_updated(self, widget):
        style = widget.get_style_context()
64 65 66
        self._setup_default_styles(style)

    def _setup_default_styles(self, style=None):
67 68
        roman, italic = Pango.Style.NORMAL, Pango.Style.ITALIC
        normal, bold = Pango.Weight.NORMAL, Pango.Weight.BOLD
69

70 71 72 73 74 75 76
        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")
77

78
        self.text_attributes = [
79 80 81 82 83 84 85 86 87
            # 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
88
            (mod_fg, roman,  normal, None),  # STATE_RENAMED
89 90
            (con_fg, roman,  bold,   None),  # STATE_CONFLICT
            (del_fg, roman,  bold,   True),  # STATE_REMOVED
91 92
            (del_fg, roman,  bold,   True),  # STATE_MISSING
            (unk_fg, roman,  normal, True),  # STATE_NONEXIST
93
        ]
94

95 96
        self.icon_details = [
            # file-icon, folder-icon, file-tint, folder-tint
97 98 99 100
            ("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
101 102
            ("dialog-warning", None,     None,   None),    # ERROR
            (None,             None,     None,   None),    # EMPTY
103 104
            ("text-x-generic", "folder", new_fg, None),    # NEW
            ("text-x-generic", "folder", mod_fg, None),    # MODIFIED
105
            ("text-x-generic", "folder", mod_fg, None),    # RENAMED
106 107 108
            ("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
109
            ("text-x-generic", "folder", unk_fg, unk_fg),  # NONEXIST
110
        ]
111

112
        assert len(self.icon_details) == len(self.text_attributes) == STATE_MAX
113 114 115 116 117

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

    def value_path(self, it, pane):
118
        return self.get_value(it, self.column_index(COL_PATH, pane))
119

120 121 122 123 124 125
    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)

126 127
    def column_index(self, col, pane):
        return self.ntree * col + pane
steve9000's avatar
steve9000 committed
128 129

    def add_entries(self, parent, names):
130
        it = self.append(parent)
131
        for pane, path in enumerate(names):
132
            self.unsafe_set(it, pane, {COL_PATH: path})
133
        return it
steve9000's avatar
steve9000 committed
134 135

    def add_empty(self, parent, text="empty folder"):
136 137 138
        it = self.append(parent)
        for pane in range(self.ntree):
            self.set_state(it, pane, STATE_EMPTY, text)
139
        return it
steve9000's avatar
steve9000 committed
140

141
    def add_error(self, parent, msg, pane, defaults={}):
142
        it = self.append(parent)
143 144
        key_values = {COL_STATE: str(STATE_ERROR)}
        key_values.update(defaults)
steve9000's avatar
steve9000 committed
145
        for i in range(self.ntree):
146
            self.unsafe_set(it, i, key_values)
147
        self.set_state(it, pane, STATE_ERROR, msg)
Stephen Kennedy's avatar
Stephen Kennedy committed
148

149 150 151 152 153
    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)
154 155

    def set_state(self, it, pane, state, label, isdir=0):
Kai Willadsen's avatar
Kai Willadsen committed
156 157 158
        icon = self.icon_details[state][1 if isdir else 0]
        tint = self.icon_details[state][3 if isdir else 2]
        fg, style, weight, strike = self.text_attributes[state]
159 160 161 162 163 164 165 166 167
        self.unsafe_set(it, pane, {
            COL_STATE: str(state),
            COL_TEXT: label,
            COL_ICON: icon,
            COL_TINT: tint,
            COL_FG: fg,
            COL_STYLE: style,
            COL_WEIGHT: weight,
            COL_STRIKE: strike
168
        })
169

170
    def get_state(self, it, pane):
171
        state_idx = self.column_index(COL_STATE, pane)
172
        try:
173
            return int(self.get_value(it, state_idx))
174 175
        except TypeError:
            return None
176

177
    def _find_next_prev_diff(self, start_path):
178 179
        def match_func(it):
            # TODO: It works, but matching on the first pane only is very poor
180 181
            return self.get_state(it, 0) not in (
                STATE_NORMAL, STATE_NOCHANGE, STATE_EMPTY)
182

183
        return self.get_previous_next_paths(start_path, match_func)
184

185 186 187 188 189 190 191 192 193 194
    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
195

196
    def unsafe_set(self, treeiter, pane, keys_values):
197 198 199 200 201 202 203 204 205 206 207 208 209
        """ This must be fastest than super.set,
        at the cost that may crash the application if you don't
        know what your're passing here.
        ie: pass treeiter or column as None crash meld

        treeiter: Gtk.TreeIter
        keys_values: dict<column, value>
            column: Int col index
            value: Str (UTF-8), Int, Float, Double, Boolean, None or GObject

        return None
        """
        safe_keys_values = {
210 211 212
            self.column_index(col, pane):
            val if val is not None
            else self._none_of_cols.get(self.column_index(col, pane))
213 214 215 216 217 218 219 220 221
            for col, val in keys_values.items()
        }
        if _GIGtk and treeiter:
            columns = [col for col in safe_keys_values.keys()]
            values = [val for val in safe_keys_values.values()]
            _GIGtk.TreeStore.set(self, treeiter, columns, values)
        else:
            self.set(treeiter, safe_keys_values)

222

223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
class TreeviewCommon:

    def on_treeview_popup_menu(self, treeview):
        cursor_path, cursor_col = treeview.get_cursor()
        if not cursor_path:
            self.popup_menu.popup_at_pointer(None)
            return True

        # We always want to pop up to the right of the first column,
        # ignoring the actual cursor column location.
        rect = treeview.get_background_area(
            cursor_path, treeview.get_column(0))

        self.popup_menu.popup_at_rect(
            treeview.get_bin_window(),
            rect,
            Gdk.Gravity.SOUTH_EAST,
            Gdk.Gravity.NORTH_WEST,
            None,
        )
        return True

    def on_treeview_button_press_event(self, treeview, event):

        # If we have multiple treeviews, unselect clear other tree selections
        num_panes = getattr(self, 'num_panes', 1)
        if num_panes > 1:
            for t in self.treeview[:self.num_panes]:
                if t != treeview:
                    t.get_selection().unselect_all()

        if (event.triggers_context_menu() and
                event.type == Gdk.EventType.BUTTON_PRESS):

            treeview.grab_focus()

            path = treeview.get_path_at_pos(int(event.x), int(event.y))
            if path is None:
                return False

            selection = treeview.get_selection()
            model, rows = selection.get_selected_rows()

            if path[0] not in rows:
                selection.unselect_all()
                selection.select_path(path[0])
                treeview.set_cursor(path[0])

            self.popup_menu.popup_at_pointer(event)
            return True
        return False


276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
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