Merge lp:~stephen-j-boddy/terminator/feature-remember-focus into lp:terminator/trunk

Proposed by Stephen Boddy
Status: Merged
Merged at revision: 1490
Proposed branch: lp:~stephen-j-boddy/terminator/feature-remember-focus
Merge into: lp:terminator/trunk
Diff against target: 443 lines (+186/-28)
6 files modified
terminatorlib/container.py (+15/-3)
terminatorlib/notebook.py (+89/-14)
terminatorlib/terminal.py (+19/-9)
terminatorlib/terminator.py (+49/-0)
terminatorlib/util.py (+3/-1)
terminatorlib/window.py (+11/-1)
To merge this branch: bzr merge lp:~stephen-j-boddy/terminator/feature-remember-focus
Reviewer Review Type Date Requested Status
Terminator Pending
Review via email: mp+199654@code.launchpad.net

Description of the change

OK, This took longer and was more tricky than I first anticipated. When loading/saving layouts, Terminator now remembers which:
1) was the active window,
2) terminal was active in non-tabbed windows,
3) tab was current in tabbed windows,
4) terminal was active in each tab (also remembered when switching between tabs at runtime),
5) UUID was assigned to a terminal, and restores it.

(5) means you can rely on the shell environment variable TERMINATOR_UUID to always be the same on relaunching a layout.

I tend to dogfood everything I code, but I'd appreciate it if some others could download and give this a workout before I go and apply to trunk.

To post a comment you must log in.
Revision history for this message
Stephen Boddy (stephen-j-boddy) wrote :

Ha, continuing my tradition of spotting errors after I submit for merging, I just noticed the following:
terminatorlib/window.py
@@ -239,6 +241,17 @@
Of that entire block of added lines, only the last two are needed. It doesn't do any harm being there, so I'll change it later. Must be something I tried while fighting to get all these other lines to do what *I* wanted.

1484. By Stephen Boddy

Remove the unnecessary cruft left over in the windows on_focus_in related to terminal focus

1485. By Stephen Boddy

Missed a line in the cruft removal

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'terminatorlib/container.py'
2--- terminatorlib/container.py 2013-09-25 22:07:01 +0000
3+++ terminatorlib/container.py 2014-01-21 22:41:47 +0000
4@@ -263,6 +263,7 @@
5
6 if hasattr(self, 'isfullscreen'):
7 layout['fullscreen'] = self.isfullscreen
8+
9 if hasattr(self, 'ratio'):
10 layout['ratio'] = self.ratio
11
12@@ -272,15 +273,26 @@
13 if hasattr(self, 'title'):
14 layout['title'] = self.title.text
15
16- labels = []
17 if mytype == 'Notebook':
18+ labels = []
19+ last_active_term = []
20 for tabnum in xrange(0, self.get_n_pages()):
21 page = self.get_nth_page(tabnum)
22 label = self.get_tab_label(page)
23 labels.append(label.get_custom_label())
24+ last_active_term.append(self.last_active_term[self.get_nth_page(tabnum)])
25+ layout['labels'] = labels
26+ layout['last_active_term'] = last_active_term
27 layout['active_page'] = self.get_current_page()
28- if len(labels) > 0:
29- layout['labels'] = labels
30+ else:
31+ if hasattr(self, 'last_active_term') and self.last_active_term is not None:
32+ layout['last_active_term'] = self.last_active_term
33+
34+ if mytype == 'Window':
35+ if self.uuid == self.terminator.last_active_window:
36+ layout['last_active_window'] = True
37+ else:
38+ layout['last_active_window'] = False
39
40 name = 'child%d' % count
41 count = count + 1
42
43=== modified file 'terminatorlib/notebook.py'
44--- terminatorlib/notebook.py 2013-10-25 14:55:26 +0000
45+++ terminatorlib/notebook.py 2014-01-21 22:41:47 +0000
46@@ -12,11 +12,14 @@
47 from container import Container
48 from editablelabel import EditableLabel
49 from translation import _
50-from util import err, dbg, enumerate_descendants
51+from util import err, dbg, enumerate_descendants, make_uuid
52
53 class Notebook(Container, gtk.Notebook):
54 """Class implementing a gtk.Notebook container"""
55 window = None
56+ last_active_term = None
57+ pending_on_tab_switch = None
58+ pending_on_tab_switch_args = None
59
60 def __init__(self, window):
61 """Class initialiser"""
62@@ -30,12 +33,16 @@
63 self.window = window
64 gobject.type_register(Notebook)
65 self.register_signals(Notebook)
66+ self.connect('switch-page', self.deferred_on_tab_switch)
67 self.configure()
68
69 child = window.get_child()
70 window.remove(child)
71 window.add(self)
72 self.newtab(widget=child)
73+ if window.last_active_term:
74+ self.set_last_active_term(window.last_active_term)
75+ window.last_active_term = None
76
77 self.show_all()
78
79@@ -63,6 +70,7 @@
80 style.xthickness = 0
81 style.ythickness = 0
82 self.modify_style(style)
83+ self.last_active_term = {}
84
85 def create_layout(self, layout):
86 """Apply layout configuration"""
87@@ -117,10 +125,14 @@
88 label = self.get_tab_label(page)
89 label.set_custom_label(labeltext)
90 page.create_layout(children[child_key])
91+
92+ if layout.get('last_active_term', None):
93+ self.last_active_term[page] = make_uuid(layout['last_active_term'][num])
94 num = num + 1
95
96 if layout.has_key('active_page'):
97- self.set_current_page(int(layout['active_page']))
98+ # Need to do it later, or layout changes result
99+ gobject.idle_add(self.set_current_page, int(layout['active_page']))
100 else:
101 self.set_current_page(0)
102
103@@ -167,12 +179,13 @@
104 self.set_current_page(page_num)
105
106 self.show_all()
107- terminal.grab_focus()
108
109 while gtk.events_pending():
110 gtk.main_iteration_do(False)
111 self.get_toplevel().set_pos_by_ratio = False
112
113+ gobject.idle_add(terminal.ensure_visible_and_focussed)
114+
115 def add(self, widget, metadata=None):
116 """Add a widget to the container"""
117 dbg('adding a new tab')
118@@ -268,15 +281,22 @@
119
120 dbg('inserting page at position: %s' % tabpos)
121 self.insert_page(widget, None, tabpos)
122- self.set_tab_label(widget, label)
123- self.set_tab_label_packing(widget, not self.config['scroll_tabbar'],
124- not self.config['scroll_tabbar'],
125- gtk.PACK_START)
126+ child_widgets = [widget]
127+ child_widgets .extend(enumerate_descendants(widget))
128+ term_widget = None
129+ for term_widget in child_widgets:
130+ if maker.isinstance(term_widget, 'Terminal'):
131+ self.set_last_active_term(term_widget.uuid)
132+ self.set_tab_label(term_widget, label)
133+ self.set_tab_label_packing(term_widget, not self.config['scroll_tabbar'],
134+ not self.config['scroll_tabbar'],
135+ gtk.PACK_START)
136+ break
137
138 self.set_tab_reorderable(widget, True)
139 self.set_current_page(tabpos)
140 self.show_all()
141- if maker.isinstance(widget, 'Terminal'):
142+ if maker.isinstance(term_widget, 'Terminal'):
143 widget.grab_focus()
144
145 def wrapcloseterm(self, widget):
146@@ -311,6 +331,7 @@
147
148 if maker.isinstance(child, 'Terminal'):
149 dbg('Notebook::closetab: child is a single Terminal')
150+ del nb.last_active_term[child]
151 child.close()
152 # FIXME: We only do this del and return here to avoid removing the
153 # page below, which child.close() implicitly does
154@@ -318,9 +339,7 @@
155 return
156 elif maker.isinstance(child, 'Container'):
157 dbg('Notebook::closetab: child is a Container')
158- dialog = self.construct_confirm_close(self.window, _('tab'))
159- result = dialog.run()
160- dialog.destroy()
161+ result = self.construct_confirm_close(self.window, _('tab'))
162
163 if result == gtk.RESPONSE_ACCEPT:
164 containers = None
165@@ -340,9 +359,6 @@
166 err('Notebook::closetab: child is unknown type %s' % child)
167 return
168
169- nb.remove_page(tabnum)
170- del(label)
171-
172 def resizeterm(self, widget, keyname):
173 """Handle a keyboard event requesting a terminal resize"""
174 raise NotImplementedError('resizeterm')
175@@ -403,6 +419,65 @@
176 terms = parent.get_visible_terminals()
177 terms.keys()[-1].grab_focus()
178
179+ def page_num_descendant(self, widget):
180+ """Find the tabnum of the tab containing a widget at any level"""
181+ tabnum = self.page_num(widget)
182+ dbg("widget is direct child if not equal -1 - tabnum: %d" % tabnum)
183+ while tabnum == -1 and widget.get_parent():
184+ widget = widget.get_parent()
185+ tabnum = self.page_num(widget)
186+ dbg("found tabnum containing widget: %d" % tabnum)
187+ return tabnum
188+
189+ def set_last_active_term(self, uuid):
190+ """Set the last active term for uuid"""
191+ widget = self.terminator.find_terminal_by_uuid(uuid.urn)
192+ if not widget:
193+ err("Cannot find terminal with uuid: %s, so cannot make it active" % (uuid.urn))
194+ return
195+ tabnum = self.page_num_descendant(widget)
196+ if tabnum == -1:
197+ err("No tabnum found for terminal with uuid: %s" % (uuid.urn))
198+ return
199+ nth_page = self.get_nth_page(tabnum)
200+ self.last_active_term[nth_page] = uuid
201+
202+ def clean_last_active_term(self):
203+ """Clean up old entries in last_active_term"""
204+ if self.terminator.doing_layout == True:
205+ return
206+ last_active_term = {}
207+ for tabnum in xrange(0, self.get_n_pages()):
208+ nth_page = self.get_nth_page(tabnum)
209+ if nth_page in self.last_active_term:
210+ last_active_term[nth_page] = self.last_active_term[nth_page]
211+ self.last_active_term = last_active_term
212+
213+ def deferred_on_tab_switch(self, notebook, page, page_num, data=None):
214+ """Prime a single idle tab switch signal, using the most recent set of params"""
215+ tabs_last_active_term = self.last_active_term.get(self.get_nth_page(page_num), None)
216+ data = {'tabs_last_active_term':tabs_last_active_term}
217+
218+ self.pending_on_tab_switch_args = (notebook, page, page_num, data)
219+ if self.pending_on_tab_switch == True:
220+ return
221+ gobject.idle_add(self.do_deferred_on_tab_switch)
222+ self.pending_on_tab_switch = True
223+
224+ def do_deferred_on_tab_switch(self):
225+ """Perform the latest tab switch signal, and resetting the pending flag"""
226+ self.on_tab_switch(*self.pending_on_tab_switch_args)
227+ self.pending_on_tab_switch = False
228+ self.pending_on_tab_switch_args = None
229+
230+ def on_tab_switch(self, notebook, page, page_num, data=None):
231+ """Do the real work for a tab switch"""
232+ tabs_last_active_term = data['tabs_last_active_term']
233+ if tabs_last_active_term:
234+ term = self.terminator.find_terminal_by_uuid(tabs_last_active_term.urn)
235+ gobject.idle_add(term.ensure_visible_and_focussed)
236+ return True
237+
238 class TabLabel(gtk.HBox):
239 """Class implementing a label widget for Notebook tabs"""
240 notebook = None
241
242=== modified file 'terminatorlib/terminal.py'
243--- terminatorlib/terminal.py 2013-10-25 14:57:14 +0000
244+++ terminatorlib/terminal.py 2014-01-21 22:41:47 +0000
245@@ -15,7 +15,7 @@
246 import subprocess
247 import urllib
248
249-from util import dbg, err, gerr, spawn_new_terminator
250+from util import dbg, err, gerr, spawn_new_terminator, make_uuid
251 import util
252 from config import Config
253 from cwd import get_default_cwd
254@@ -1069,13 +1069,12 @@
255 maker = Factory()
256
257 if maker.isinstance(topchild, 'Notebook'):
258- prevtmp = None
259- tmp = self.get_parent()
260- while tmp != topchild:
261- prevtmp = tmp
262- tmp = tmp.get_parent()
263- page = topchild.page_num(prevtmp)
264- topchild.set_current_page(page)
265+ # Find which page number this term is on
266+ tabnum = topchild.page_num_descendant(self)
267+ # If terms page number is not the current one, switch to it
268+ current_page = topchild.get_current_page()
269+ if tabnum != current_page:
270+ topchild.set_current_page(tabnum)
271
272 self.grab_focus()
273
274@@ -1088,7 +1087,15 @@
275 self.vte.set_colors(self.fgcolor_active, self.bgcolor,
276 self.palette_active)
277 self.set_cursor_color()
278- self.terminator.last_focused_term = self
279+ if not self.terminator.doing_layout:
280+ self.terminator.last_focused_term = self
281+ if self.get_toplevel().is_child_notebook():
282+ notebook = self.get_toplevel().get_children()[0]
283+ notebook.set_last_active_term(self.uuid)
284+ notebook.clean_last_active_term()
285+ self.get_toplevel().last_active_term = None
286+ else:
287+ self.get_toplevel().last_active_term = self.uuid
288 self.emit('focus-in')
289
290 def on_vte_focus_out(self, _widget, _event):
291@@ -1490,6 +1497,7 @@
292 title = self.titlebar.get_custom_string()
293 if title:
294 layout['title'] = title
295+ layout['uuid'] = self.uuid
296 name = 'terminal%d' % count
297 count = count + 1
298 global_layout[name] = layout
299@@ -1511,6 +1519,8 @@
300 self.titlebar.set_custom_string(layout['title'])
301 if layout.has_key('directory') and layout['directory'] != '':
302 self.directory = layout['directory']
303+ if layout.has_key('uuid') and layout['uuid'] != '':
304+ self.uuid = make_uuid(layout['uuid'])
305
306 def scroll_by_page(self, pages):
307 """Scroll up or down in pages"""
308
309=== modified file 'terminatorlib/terminator.py'
310--- terminatorlib/terminator.py 2013-09-04 20:59:27 +0000
311+++ terminatorlib/terminator.py 2014-01-21 22:41:47 +0000
312@@ -34,6 +34,7 @@
313 debug_address = None
314
315 doing_layout = None
316+ last_active_window = None
317
318 groupsend = None
319 groupsend_type = {'all':0, 'group':1, 'off':2}
320@@ -291,11 +292,59 @@
321 def layout_done(self):
322 """Layout operations have finished, record that fact"""
323 self.doing_layout = False
324+ maker = Factory()
325+
326+ window_last_active_term_mapping = {}
327+ for window in self.windows:
328+ if window.is_child_notebook():
329+ source = window.get_toplevel().get_children()[0]
330+ else:
331+ source = window
332+ window_last_active_term_mapping[window] = copy.copy(source.last_active_term)
333
334 for terminal in self.terminals:
335 if not terminal.pid:
336 terminal.spawn_child()
337
338+ for window in self.windows:
339+ if window.is_child_notebook():
340+ # For windows with a notebook
341+ notebook = window.get_toplevel().get_children()[0]
342+ # Cycle through pages by number
343+ for page in xrange(0, notebook.get_n_pages()):
344+ # Try and get the entry in the previously saved mapping
345+ mapping = window_last_active_term_mapping[window]
346+ page_last_active_term = mapping.get(notebook.get_nth_page(page), None)
347+ if page_last_active_term is None:
348+ # Couldn't find entry, so we find the first child of type Terminal
349+ children = notebook.get_nth_page(page).get_children()
350+ for page_last_active_term in children:
351+ if maker.isinstance(page_last_active_term, 'Terminal'):
352+ page_last_active_term = page_last_active_term.uuid
353+ break
354+ else:
355+ err('Should never reach here!')
356+ page_last_active_term = None
357+ if page_last_active_term is None:
358+ # Bail on this tab as we're having no luck here, continue with the next
359+ continue
360+ # Set the notebook entry, then ensure Terminal is visible and focussed
361+ urn = page_last_active_term.urn
362+ notebook.last_active_term[notebook.get_nth_page(page)] = page_last_active_term
363+ if urn:
364+ term = self.find_terminal_by_uuid(urn)
365+ if term:
366+ term.ensure_visible_and_focussed()
367+ else:
368+ # For windows without a notebook ensure Terminal is visible and focussed
369+ if window_last_active_term_mapping[window]:
370+ term = self.find_terminal_by_uuid(window_last_active_term_mapping[window].urn)
371+ term.ensure_visible_and_focussed()
372+
373+ for window in self.windows:
374+ if window.uuid == self.last_active_window:
375+ window.show()
376+
377 def reconfigure(self):
378 """Update configuration for the whole application"""
379
380
381=== modified file 'terminatorlib/util.py'
382--- terminatorlib/util.py 2013-08-28 21:09:17 +0000
383+++ terminatorlib/util.py 2014-01-21 22:41:47 +0000
384@@ -277,8 +277,10 @@
385 len(terminals), parent))
386 return(containers, terminals)
387
388-def make_uuid():
389+def make_uuid(str_uuid=None):
390 """Generate a UUID for an object"""
391+ if str_uuid:
392+ return uuid.UUID(str_uuid)
393 return uuid.uuid4()
394
395 def inject_uuid(target):
396
397=== modified file 'terminatorlib/window.py'
398--- terminatorlib/window.py 2013-10-25 14:57:14 +0000
399+++ terminatorlib/window.py 2014-01-21 22:41:47 +0000
400@@ -5,12 +5,13 @@
401
402 import copy
403 import time
404+import uuid
405 import pygtk
406 pygtk.require('2.0')
407 import gobject
408 import gtk
409
410-from util import dbg, err
411+from util import dbg, err, make_uuid
412 import util
413 from translation import _
414 from version import APP_NAME
415@@ -38,6 +39,7 @@
416 position = None
417 ignore_startup_show = None
418 set_pos_by_ratio = None
419+ last_active_term = None
420
421 zoom_data = None
422
423@@ -239,6 +241,8 @@
424 def on_focus_in(self, window, event):
425 """Focus has entered the window"""
426 self.set_urgency_hint(False)
427+ if not self.terminator.doing_layout:
428+ self.terminator.last_active_window = self.uuid
429 # FIXME: Cause the terminal titlebars to update here
430
431 def is_child_notebook(self):
432@@ -865,6 +869,12 @@
433
434 self.get_children()[0].create_layout(child)
435
436+ if layout.has_key('last_active_term') and layout['last_active_term'] not in ['', None]:
437+ self.last_active_term = make_uuid(layout['last_active_term'])
438+
439+ if layout.has_key('last_active_window') and layout['last_active_window'] == 'True':
440+ self.terminator.last_active_window = self.uuid
441+
442 class WindowTitle(object):
443 """Class to handle the setting of the window title"""
444