widgets.browserview: Try to fix crash
[ranger.git] / ranger / gui / widgets / browserview.py
blob84f77de61b0c1f1898fb87d8f3152feb4e6c0390
1 # Copyright (C) 2009, 2010 Roman Zimbelmann <romanz@lavabit.com>
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 """The BrowserView manages a set of BrowserColumns."""
17 import curses
18 from ranger.ext.signals import Signal
19 from ranger.ext.keybinding_parser import key_to_string
20 from . import Widget
21 from .browsercolumn import BrowserColumn
22 from .pager import Pager
23 from ..displayable import DisplayableContainer
25 class BrowserView(Widget, DisplayableContainer):
26 ratios = None
27 preview = True
28 is_collapsed = False
29 draw_bookmarks = False
30 stretch_ratios = None
31 need_clear = False
32 old_collapse = False
33 draw_hints = False
35 def __init__(self, win, ratios, preview = True):
36 DisplayableContainer.__init__(self, win)
37 self.preview = preview
38 self.columns = []
40 self.pager = Pager(self.win, embedded=True)
41 self.pager.visible = False
42 self.add_child(self.pager)
44 self.change_ratios(ratios, resize=False)
46 for option in ('preview_directories', 'preview_files'):
47 self.settings.signal_bind('setopt.' + option,
48 self._request_clear_if_has_borders, weak=True)
50 self.fm.env.signal_bind('move', self.request_clear)
51 self.settings.signal_bind('setopt.column_ratios', self.request_clear)
53 def change_ratios(self, ratios, resize=True):
54 if isinstance(ratios, Signal):
55 ratios = ratios.value
57 for column in self.columns:
58 column.destroy()
59 self.remove_child(column)
60 self.columns = []
62 ratio_sum = float(sum(ratios))
63 self.ratios = tuple(x / ratio_sum for x in ratios)
65 last = 0.1 if self.settings.padding_right else 0
66 if len(self.ratios) >= 2:
67 self.stretch_ratios = self.ratios[:-2] + \
68 ((self.ratios[-2] + self.ratios[-1] * 1.0 - last),
69 (self.ratios[-1] * last))
71 offset = 1 - len(ratios)
72 if self.preview: offset += 1
74 for level in range(len(ratios)):
75 fl = BrowserColumn(self.win, level + offset)
76 self.add_child(fl)
77 self.columns.append(fl)
79 try:
80 self.main_column = self.columns[self.preview and -2 or -1]
81 except IndexError:
82 self.main_column = None
83 else:
84 self.main_column.display_infostring = True
85 self.main_column.main_column = True
87 self.resize(self.y, self.x, self.hei, self.wid)
89 def _request_clear_if_has_borders(self):
90 if self.settings.draw_borders:
91 self.request_clear()
93 def request_clear(self):
94 self.need_clear = True
96 def draw(self):
97 if self.need_clear:
98 self.win.erase()
99 self.need_redraw = True
100 self.need_clear = False
101 for path in self.fm.tabs.values():
102 if path is not None:
103 directory = self.env.get_directory(path)
104 directory.load_content_if_outdated()
105 directory.use()
106 DisplayableContainer.draw(self)
107 if self.settings.draw_borders:
108 self._draw_borders()
109 if self.draw_bookmarks:
110 self._draw_bookmarks()
111 elif self.draw_hints:
112 self._draw_hints()
114 def finalize(self):
115 if self.pager.visible:
116 try:
117 self.fm.ui.win.move(self.main_column.y, self.main_column.x)
118 except:
119 pass
120 else:
121 try:
122 x = self.main_column.x
123 y = self.main_column.y + self.main_column.target.pointer\
124 - self.main_column.scroll_begin
125 self.fm.ui.win.move(y, x)
126 except:
127 pass
129 def _draw_borders(self):
130 win = self.win
131 self.color('in_browser', 'border')
133 left_start = 0
134 right_end = self.wid - 1
136 for child in self.columns:
137 if not child.has_preview():
138 left_start = child.x + child.wid
139 else:
140 break
141 if not self.pager.visible:
142 for child in reversed(self.columns):
143 if not child.has_preview():
144 right_end = child.x - 1
145 else:
146 break
147 if right_end < left_start:
148 right_end = self.wid - 1
150 win.hline(0, left_start, curses.ACS_HLINE, right_end - left_start)
151 win.hline(self.hei - 1, left_start, curses.ACS_HLINE,
152 right_end - left_start)
153 win.vline(1, left_start, curses.ACS_VLINE, self.hei - 2)
155 for child in self.columns:
156 if not child.has_preview():
157 continue
158 if child.main_column and self.pager.visible:
159 win.vline(1, right_end, curses.ACS_VLINE, self.hei - 2)
160 break
161 x = child.x + child.wid
162 y = self.hei - 1
163 try:
164 win.vline(1, x, curses.ACS_VLINE, y - 1)
165 win.addch(0, x, curses.ACS_TTEE, 0)
166 win.addch(y, x, curses.ACS_BTEE, 0)
167 except:
168 # in case it's off the boundaries
169 pass
171 self.addch(0, left_start, curses.ACS_ULCORNER)
172 self.addch(self.hei - 1, left_start, curses.ACS_LLCORNER)
173 self.addch(0, right_end, curses.ACS_URCORNER)
174 self.addch(self.hei - 1, right_end, curses.ACS_LRCORNER)
176 def _draw_bookmarks(self):
177 self.fm.bookmarks.update_if_outdated()
178 self.color_reset()
179 self.need_clear = True
181 sorted_bookmarks = sorted((item for item in self.fm.bookmarks \
182 if self.fm.settings.show_hidden_bookmarks or \
183 '/.' not in item[1].path), key=lambda t: t[0].lower())
185 hei = min(self.hei - 1, len(sorted_bookmarks))
186 ystart = self.hei - hei
188 maxlen = self.wid
189 self.addnstr(ystart - 1, 0, "mark path".ljust(self.wid), self.wid)
191 whitespace = " " * maxlen
192 for line, items in zip(range(self.hei-1), sorted_bookmarks):
193 key, mark = items
194 string = " " + key + " " + mark.path
195 self.addstr(ystart + line, 0, whitespace)
196 self.addnstr(ystart + line, 0, string, self.wid)
198 self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
200 def _draw_hints(self):
201 self.need_clear = True
202 hints = []
203 for k, v in self.fm.env.keybuffer.pointer.items():
204 k = key_to_string(k)
205 if isinstance(v, dict):
206 text = '...'
207 else:
208 text = v
209 if text.startswith('hint') or text.startswith('chain hint'):
210 continue
211 hints.append((k, text))
212 hints.sort(key=lambda t: t[1])
214 hei = min(self.hei - 1, len(hints))
215 ystart = self.hei - hei
216 self.addnstr(ystart - 1, 0, "key command".ljust(self.wid),
217 self.wid)
218 self.win.chgat(ystart - 1, 0, curses.A_UNDERLINE)
219 whitespace = " " * self.wid
220 i = ystart
221 for key, cmd in hints:
222 string = " " + key.ljust(11) + " " + cmd
223 self.addstr(i, 0, whitespace)
224 self.addnstr(i, 0, string, self.wid)
225 i += 1
227 def _collapse(self):
228 # Should the last column be cut off? (Because there is no preview)
229 if not self.settings.collapse_preview or not self.preview \
230 or not self.stretch_ratios:
231 return False
232 result = not self.columns[-1].has_preview()
233 target = self.columns[-1].target
234 if not result and target and target.is_file and \
235 self.fm.settings.preview_script and \
236 self.fm.settings.use_preview_script:
237 try:
238 result = not self.fm.previews[target.realpath]['foundpreview']
239 except:
240 return self.old_collapse
241 self.old_collapse = result
242 return result
244 def resize(self, y, x, hei, wid):
245 """Resize all the columns according to the given ratio"""
246 DisplayableContainer.resize(self, y, x, hei, wid)
247 borders = self.settings.draw_borders
248 pad = 1 if borders else 0
249 left = pad
251 self.is_collapsed = self._collapse()
252 if self.is_collapsed:
253 generator = enumerate(self.stretch_ratios)
254 else:
255 generator = enumerate(self.ratios)
257 last_i = len(self.ratios) - 1
259 for i, ratio in generator:
260 wid = int(ratio * self.wid)
262 cut_off = self.is_collapsed and not self.settings.padding_right
263 if i == last_i:
264 if not cut_off:
265 wid = int(self.wid - left + 1 - pad)
266 else:
267 self.columns[i].resize(pad, left - 1, hei - pad * 2, 1)
268 self.columns[i].visible = False
269 continue
271 if i == last_i - 1:
272 self.pager.resize(pad, left, hei - pad * 2, \
273 max(1, self.wid - left - pad))
275 if cut_off:
276 self.columns[i].resize(pad, left, hei - pad * 2, \
277 max(1, self.wid - left - pad))
278 continue
280 try:
281 self.columns[i].resize(pad, left, hei - pad * 2, \
282 max(1, wid - 1))
283 except KeyError:
284 pass
286 left += wid
288 def click(self, event):
289 if DisplayableContainer.click(self, event):
290 return True
291 direction = event.mouse_wheel_direction()
292 if direction:
293 self.main_column.scroll(direction)
294 return False
296 def open_pager(self):
297 self.pager.visible = True
298 self.pager.focused = True
299 self.need_clear = True
300 self.pager.open()
301 try:
302 self.columns[-1].visible = False
303 self.columns[-2].visible = False
304 except IndexError:
305 pass
307 def close_pager(self):
308 self.pager.visible = False
309 self.pager.focused = False
310 self.need_clear = True
311 self.pager.close()
312 try:
313 self.columns[-1].visible = True
314 self.columns[-2].visible = True
315 except IndexError:
316 pass
318 def poke(self):
319 DisplayableContainer.poke(self)
321 # Show the preview column when it has a preview but has
322 # been hidden (e.g. because of padding_right = False)
323 if not self.pager.visible and not self.columns[-1].visible and \
324 self.columns[-1].target and self.columns[-1].target.is_directory \
325 or self.columns[-1].has_preview() and not self.pager.visible:
326 self.columns[-1].visible = True
328 if self.preview and self.is_collapsed != self._collapse():
329 self.resize(self.y, self.x, self.hei, self.wid)