1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2011 by Andrew Chadwick <andrewc-git@piffle.org>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 """Manage a main window with a sidebar, plus a number of subwindows.
10 Tools can be snapped in and out of the sidebar.
11 Window sizes and sidebar positions are stored to user preferences.
18 from warnings import warn
21 from gettext import gettext as _
25 """Keeps track of tool positions, and main window state.
28 def __init__(self, app, factory, factory_opts=[], prefs=None):
32 should be a ref to a dict-like object. Persistent prefs
33 will be read from and written back to it.
36 is a callable which has the signature
38 factory(role, layout_manager, *factory_opts)
40 and which will be called for each needed tool. It should
41 return a value of the form:
46 4. (tool_widget, title_str)
48 Form 1 indicates that no matching widget could be found.
49 Form 2 should be used for floating dialog windows, popup
50 windows and similar, or for "main-window". Form 3 is
51 expected when the "role" parameter is "main-widget"; the
52 returned widgets are packed into the main window.
59 self.window_group = gtk.WindowGroup()
60 self.factory = factory
61 self.factory_opts = factory_opts
62 self.widgets = {} # {role: <gtk.Widget>} as returned by factory()
63 self.tools = {} # {role: Tool}
64 self.subwindows = {} # {role: <gtk.Window>}
65 self.main_window = None
66 self.saved_user_tools = []
67 self.tool_visibility_observers = []
69 def set_main_window_title(self, title):
70 """Set the title for the main window.
72 self.main_window.set_title(title)
74 def get_widget_by_role(self, role):
75 """Returns the raw widget for a particular role name.
77 Internally cached; will invoke the factory method once for each
78 unique role name the first time it's seen.
80 if role in self.widgets:
81 return self.widgets[role]
83 result = self.factory(role, self, *self.factory_opts)
84 if role == 'main-window':
85 self.main_window = result[0]
86 assert isinstance(self.main_window, MainWindow)
87 self.drag_state = ToolDragState(self)
88 # ^^^ Yuck. Would be nicer to init in constructor
89 self.widgets[role] = self.main_window
90 return self.main_window
91 elif role == 'main-widget':
93 self.widgets[role] = widget
99 if isinstance(widget, gtk.Window):
100 self.subwindows[role] = widget
101 self.widgets[role] = widget
102 widget.set_role(role)
103 widget.set_transient_for(self.main_window)
105 elif isinstance(widget, gtk.Widget):
107 tool = Tool(widget, role, title, title, self)
108 self.tools[role] = tool
109 self.widgets[role] = tool
112 self.widgets[role] = None
113 warn("Unknown role \"%s\"" % role, RuntimeWarning,
117 def get_subwindow_by_role(self, role):
118 """Returns the managed subwindow for a given role, or None
120 if self.get_widget_by_role(role):
121 return self.subwindows.get(role, None)
124 def get_tool_by_role(self, role):
125 """Returns the tool wrapper for a given role.
127 Only valid for roles for which a corresponding packable widget was
128 created by the factory method.
130 #newly_loaded = role not in self.widgets
131 _junk = self.get_widget_by_role(role)
132 tool = self.tools.get(role, None)
133 #if tool is not None and newly_loaded:
134 # hidden = self.get_window_hidden_by_role(tool.role)
135 # floating = self.get_window_floating_by_role(tool.role)
136 # tool.set_floating(floating)
137 # tool.set_hidden(hidden, reason="newly-loaded-prefs")
138 # # XXX move the above to get_widget_by_role()?
141 def get_window_hidden_by_role(self, role, default=True):
142 return self.prefs.get(role, {}).get("hidden", default)
144 def get_window_floating_by_role(self, role, default=False):
145 return self.prefs.get(role, {}).get("floating", default)
147 def get_tools_in_sbindex_order(self):
148 """Lists all loaded tools in order of ther sbindex setting.
150 The returned list contains all tools known to the LayoutManager
151 even if they aren't currently docked in a sidebar.
154 sbindexes = [s.get("sbindex", 0) for r, s in self.prefs.iteritems()]
156 index_hwm = len(self.tools) + max(sbindexes)
157 for role, tool in self.tools.iteritems():
158 index = self.prefs.get(role, {}).get("sbindex", index_hwm)
160 tools.append((index, tool.role, tool))
162 return [t for i,r,t in tools]
164 def toggle_user_tools(self, on=None):
165 """Temporarily toggle the user's chosen tools on or off.
172 if on or self.saved_user_tools:
173 for tool in self.saved_user_tools:
174 tool.set_hidden(False, temporary=True, reason="toggle-user-tools")
175 tool.set_floating(tool.floating)
176 self.saved_user_tools = []
177 elif off or not self.saved_user_tools:
178 for tool in self.get_tools_in_sbindex_order():
181 tool.set_hidden(True, temporary=True, reason="toggle-user-tools")
182 self.saved_user_tools.append(tool)
185 """Displays all initially visible tools.
187 # Ensure that everything not hidden in prefs is loaded up front
188 for role, win_pos in self.prefs.iteritems():
189 hidden = win_pos.get("hidden", False)
191 self.get_widget_by_role(role)
193 # Insert those that are tools into sidebar, or float them
194 # free as appropriate
196 for tool in self.get_tools_in_sbindex_order():
197 floating = self.prefs.get(tool.role, {}).get("floating", False)
199 floating_tools.append(tool)
201 self.main_window.sidebar.add_tool(tool)
202 tool.set_floating(floating)
204 self.main_window.show_all()
207 for tool in floating_tools:
208 win = tool.floating_window
209 tool.set_floating(True)
212 # Present the main window for consistency with the toggle action.
213 gobject.idle_add(self.main_window.present)
215 def notify_tool_visibility_observers(self, *args, **kwargs):
216 for func in self.tool_visibility_observers:
217 func(*args, **kwargs)
220 class ElasticContainer:
221 """Mixin for containers which mirror certain size changes of descendents
223 Descendents which wish to report internally-generated size changes should
224 add the ElasticContent mixin, and should be packed into a container that
225 derives from ElasticContainer. More than one ElasticContent widget can be
226 packed under an ElasticContainer, and each can report different types of
227 resizes - however the sub-hierarchies cannot overlap. You -can- nest
228 ElasticContainers though: it's the outer one that receives the resize
233 """Mixin constructor (construct as a gtk.Widget before calling)
235 self.__last_size = None
236 self.__saved_size_request = None
238 def mirror_elastic_content_resize(self, dx, dy):
239 """Resize by a given amount.
241 This is called by some ElasticContent widget below here in the
242 hierarchy when it notices a request-size change on itself.
246 if isinstance(p, ElasticContainer):
247 # propagate up and don't handle here
248 p.mirror_elastic_content_resize(dx, dy)
251 self.__saved_size_request = self.get_size_request()
252 alloc = self.get_allocation()
255 if isinstance(self, gtk.Window):
257 self.set_size_request(w, h)
261 class ElasticContent:
262 """Mixin for GTK widgets which want some parent to change size to match.
265 def __init__(self, mirror_vertical=True, mirror_horizontal=True):
266 """Mixin constructor (construct as a gtk.Widget before calling)
268 The options control which size changes are reported to the
269 ElasticContainer ancestor and how:
272 mirror vertical size changes
275 mirror horizontal size changes
278 self.__vertical = mirror_vertical
279 self.__horizontal = mirror_horizontal
280 if not mirror_horizontal and not mirror_vertical:
282 self.__last_req = None
283 self.__expose_connid = self.connect_after("expose-event",
284 self.__after_expose_event)
285 self.__notify_parent = False
286 self.connect_after("size-request", self.__after_size_request)
288 def __after_expose_event(self, widget, event):
289 # Begin notifying changes to the ancestor after the first expose event.
290 # It doesn't matter for widgets that know their size before hand.
291 # Assume widgets which initially don't know their size *do* know their
292 # proper size after their first draw, even if they drew themselves
293 # wrongly and now have to resize and do another size-request...
294 connid = self.__expose_connid
297 self.__expose_connid = None
298 self.__notify_parent = True
299 self.disconnect(connid)
301 def __after_size_request(self, widget, req):
302 # Catch the final value of each size-request, calculate the
303 # difference needed and throw it back up the widget hierarchy
304 # to interested parties.
305 if not self.__last_req:
308 w0, h0 = self.__last_req
309 dx, dy = req.width - w0, req.height - h0
310 self.__last_req = (req.width, req.height)
311 if not self.__notify_parent:
315 if isinstance(p, ElasticContainer):
316 if not self.__vertical:
318 if not self.__horizontal:
320 if dy != 0 or dx != 0:
321 p.mirror_elastic_content_resize(dx, dy)
323 if isinstance(p, ElasticContent):
328 class ElasticVBox (gtk.VBox, ElasticContainer):
329 def __init__(self, *args, **kwargs):
330 gtk.VBox.__init__(self, *args, **kwargs)
331 ElasticContainer.__init__(self)
334 class ElasticExpander (gtk.Expander, ElasticContent):
335 def __init__(self, *args, **kwargs):
336 gtk.Expander.__init__(self, *args, **kwargs)
337 ElasticContent.__init__(self, mirror_horizontal=False,
338 mirror_vertical=True)
341 class WindowWithSavedPosition:
342 """Mixin for gtk.Windows which load/save their position via a LayoutManager.
344 Classes using this interface must provide an attribute named layout_manager
345 which exposes the LayoutManager whose prefs attr this window will read its
346 initial position from and update. As a consequence of how this mixin
347 interacts with the LayoutManager, it must have a meaningful window role at
352 """Mixin constructor. Construction order does not matter.
354 self.__last_conf_pos = None
355 self.__pos_save_timer = None
356 self.__is_fullscreen = False
357 self.__is_maximized = False
358 self.__mapped_once = False
359 self.__initial_xy = None
360 self.connect("realize", self.__on_realize)
361 self.connect("map", self.__on_map)
362 self.connect("configure-event", self.__on_configure_event)
363 self.connect("window-state-event", self.__on_window_state_event)
367 lm = self.layout_manager
368 role = self.get_role()
369 assert role in lm.prefs, 'Window %r is not mentioned in DEFAULT_CONFIG (application.py)' % role
370 return lm.prefs[role]
372 def __on_realize(self, widget):
373 xy = set_initial_window_position(self, self.__pos)
374 self.__initial_xy = xy
376 def __force_initial_xy(self):
377 """Revolting hack to force the initial x,y position after mapping.
379 It's cosmetically necessary to do this for tool windows at least, since
380 otherwise snapping out an initially docked window using the button can
381 look really weird if the WM forces a position on you (most *ix ones
382 do), and we really do have a better idea than the WM about what the
383 user meant. The WM is given a sporting chance to help position the
384 main-window sensibly though.
386 role = self.get_role()
387 if role == 'main-window':
390 x, y = self.__initial_xy
395 def __on_map(self, widget):
396 parent = self.get_transient_for()
397 if parent is self.layout_manager.main_window:
398 self.window.set_accept_focus(False)
399 # Prevent all saved-position subwindows from taking keyboard
400 # focus from the main window (in Metacity) by presenting it
401 # again. https://gna.org/bugs/?17899
402 gobject.idle_add(parent.present)
403 ## The alternative is:
404 # gobject.idle_add(self.window.raise_)
405 # gobject.idle_add(parent.window.focus)
406 ## but that doesn't seem to be necessary.
407 if self.__mapped_once:
409 self.__mapped_once = True
410 gobject.idle_add(self.__force_initial_xy)
412 def __on_window_state_event(self, widget, event):
413 # Respond to changes of the fullscreen or maximized state only
414 interesting = gdk.WINDOW_STATE_MAXIMIZED | gdk.WINDOW_STATE_FULLSCREEN
415 if not event.changed_mask & interesting:
417 state = event.new_window_state
418 self.__is_maximized = state & gdk.WINDOW_STATE_MAXIMIZED
419 self.__is_fullscreen = state & gdk.WINDOW_STATE_FULLSCREEN
421 def __on_configure_event(self, widget, event):
422 """Store the current size of the window for future launches.
424 # Save the new position in the prefs...
425 f_ex = self.window.get_frame_extents()
428 conf_pos = dict(x=x, y=y, w=event.width, h=event.height)
429 self.__last_conf_pos = conf_pos
430 if self.get_role() == 'main-window':
431 # ... however, wait for a bit so window-state-event has a chance to
432 # fire first if the window can be meaningfully fullscreened. Compiz
433 # in particular enjoys firing up to three configure-events (at
434 # various sizes) before the window-state-event describing the
436 if not self.__pos_save_timer:
437 self.__pos_save_timer \
438 = gobject.timeout_add_seconds(2, self.__save_position_cb)
440 # Save the position now for non-main windows
441 self.__save_position_cb()
443 def __save_position_cb(self):
444 if not (self.__is_maximized or self.__is_fullscreen):
445 self.__pos.update(self.__last_conf_pos)
446 self.__pos_save_timer = None
451 class MainWindow (WindowWithSavedPosition):
452 """Mixin for main gtk.Windows in a layout.
454 Contains slots and initialisation stuff for various widget slots, which
455 can be provided by the layout manager's factory callable. These are:
464 def __init__(self, layout_manager):
465 """Mixin constructor: initialise as a gtk.Window vefore calling.
467 This builds the sidebar and packs the main UI with pieces provided
468 either by the factory or by overriding the various init_*() methods.
469 This also sets up the window with an initial position and configures
470 it to save its position and sidebar width when reconfigured to the
471 LayoutManager's prefs.
473 assert isinstance(self, gtk.Window)
474 WindowWithSavedPosition.__init__(self)
475 self.layout_manager = layout_manager
476 self.set_role("main-window")
477 self.menubar = None; self.init_menubar()
478 self.toolbar = None; self.init_toolbar()
479 self.statusbar = None; self.init_statusbar()
480 self.main_widget = None; self.init_main_widget()
481 self.sidebar = Sidebar(layout_manager)
482 self.hpaned = gtk.HPaned()
483 self.hpaned_position_loaded = False
484 self.hpaned.pack1(self.main_widget, True, False)
485 self.hpaned.pack2(self.sidebar, False, False)
486 self.layout_vbox = gtk.VBox()
487 if self.menubar is not None:
488 self.layout_vbox.pack_start(self.menubar, False, False)
489 if self.toolbar is not None:
490 self.layout_vbox.pack_start(self.toolbar, False, False)
491 self.layout_vbox.pack_start(self.hpaned, True, True)
492 if self.statusbar is not None:
493 self.layout_vbox.pack_start(self.statusbar, False, False)
494 self.add(self.layout_vbox)
495 self.last_conf_size = None
496 self.connect("configure-event", self.__on_configure_event)
497 self.sidebar.connect("size-allocate", self.__on_sidebar_size_allocate)
498 self.connect("map-event", self.__on_map_event)
500 def __on_map_event(self, widget, event):
501 if self.sidebar.is_empty():
504 def init_menubar(self):
505 self.menubar = self.layout_manager.get_widget_by_role("main-menubar")
507 def init_toolbar(self):
508 self.toolbar = self.layout_manager.get_widget_by_role("main-toolbar")
510 def init_statusbar(self):
511 self.statusbar = self.layout_manager.get_widget_by_role("main-statusbar")
513 def init_main_widget(self):
514 self.main_widget = self.layout_manager.get_widget_by_role("main-widget")
516 def __on_configure_event(self, widget, event):
517 self.last_conf_size = (event.width, event.height)
519 def __on_sidebar_size_allocate(self, sidebar, allocation):
520 lm = self.layout_manager
521 role = self.get_role()
522 if not role in lm.prefs:
524 if self.hpaned_position_loaded:
525 # Save the sidebar's width each time space is allocated to it.
526 # Responds to the user adjusting the HPaned's divider position.
527 sbwidth = allocation.width
528 self.layout_manager.prefs["main-window"]["sbwidth"] = sbwidth
529 self.layout_manager.main_window.sidebar.set_tool_widths(sbwidth)
531 # Except, ugh, the first time. If the position isn't loaded yet,
532 # load the main-window's sbwidth from the settings.
533 if self.last_conf_size:
534 width, height = self.last_conf_size
535 sbwidth = lm.prefs[role].get("sbwidth", None)
536 if sbwidth is not None:
537 handle_size = self.hpaned.style_get_property("handle-size")
538 pos = width - handle_size - sbwidth
539 self.hpaned.set_position(pos)
540 self.hpaned.queue_resize()
541 self.hpaned_position_loaded = True
544 gtk.rc_parse_string ("""
545 style "small-stock-image-button-style" {
546 GtkWidget::focus-padding = 0
547 GtkWidget::focus-line-width = 0
551 widget "*.small-stock-image-button"
552 style "small-stock-image-button-style"
556 class SmallImageButton (gtk.Button):
557 """A small button containing an image.
559 Instances are used for the close button and snap in/snap out buttons in a
563 ICON_SIZE = gtk.ICON_SIZE_MENU
565 def __init__(self, stock_id=None, tooltip=None):
566 gtk.Button.__init__(self)
568 if stock_id is not None:
569 self.set_image_from_stock(stock_id)
571 self.set_image_from_stock(gtk.STOCK_MISSING_IMAGE)
572 self.set_name("small-stock-image-button")
573 self.set_relief(gtk.RELIEF_NONE)
574 self.set_property("can-focus", False)
575 if tooltip is not None:
576 self.set_tooltip_text(tooltip)
577 self.connect("style-set", self.on_style_set)
579 def set_image_from_stock(self, stock_id):
581 self.remove(self.image)
582 self.image = gtk.image_new_from_stock(stock_id, self.ICON_SIZE)
586 # TODO: support image.set_from_icon_name maybe
588 def on_style_set(self, widget, prev_style):
589 settings = self.get_settings()
590 w, h = gtk.icon_size_lookup_for_settings(settings, gtk.ICON_SIZE_MENU)
591 self.set_size_request(w+4, h+4)
594 class ToolResizeGrip (gtk.DrawingArea):
595 """A draggable bar for resizing a Tool vertically."""
601 handle_size = gtk.HPaned().style_get_property("handle-size") + 2
602 corner_width = 4*handle_size
605 AREA_RIGHT: gdk.WINDOW_EDGE_SOUTH_EAST,
606 AREA_MIDDLE: gdk.WINDOW_EDGE_SOUTH,
607 AREA_LEFT: gdk.WINDOW_EDGE_SOUTH_WEST,
609 floating_cursor_map = {
610 AREA_LEFT: gdk.Cursor(gdk.BOTTOM_LEFT_CORNER),
611 AREA_MIDDLE: gdk.Cursor(gdk.BOTTOM_SIDE),
612 AREA_RIGHT: gdk.Cursor(gdk.BOTTOM_RIGHT_CORNER),
614 nonfloating_cursor = gdk.Cursor(gdk.SB_V_DOUBLE_ARROW)
616 def __init__(self, tool):
617 gtk.DrawingArea.__init__(self)
619 self.set_size_request(4*self.corner_width, self.handle_size)
620 self.connect("configure-event", self.on_configure_event)
621 self.connect("expose-event", self.on_expose_event)
622 self.connect("button-press-event", self.on_button_press_event)
623 self.connect("button-release-event", self.on_button_release_event)
624 self.connect("motion-notify-event", self.on_motion_notify_event)
625 self.connect("leave-notify-event", self.on_leave_notify_event)
626 self.width = self.height = self.handle_size
627 mask = gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK \
628 | gdk.LEAVE_NOTIFY_MASK | gdk.POINTER_MOTION_MASK
629 self.set_events(mask)
632 def on_configure_event(self, widget, event):
633 self.width = event.width
634 self.height = event.height
636 def on_expose_event(self, widget, event):
638 self.window.begin_paint_rect(event.area)
639 x = (self.width - self.handle_size) / 2
640 y = (self.height - self.handle_size) / 2
641 state = self.get_state()
642 self.style.paint_handle(self.window, state,
643 gtk.SHADOW_NONE, event.area, self, 'paned',
644 0, 0, self.width, self.height,
645 gtk.ORIENTATION_HORIZONTAL)
646 self.window.end_paint()
648 def get_area(self, x, y):
649 if self.tool.floating:
650 if x <= self.corner_width:
651 return self.AREA_LEFT
652 elif x >= self.width - self.corner_width:
653 return self.AREA_RIGHT
654 return self.AREA_MIDDLE
656 def on_button_press_event(self, widget, event):
657 if event.button != 1:
659 if self.tool.floating:
660 area = self.get_area(event.x, event.y)
661 win = self.tool.floating_window.window
662 edge = self.window_edge_map[area]
663 win.begin_resize_drag(edge, event.button,
664 int(event.x_root), int(event.y_root),
667 min_w, min_h = self.tool.get_minimum_size()
668 lm = self.tool.layout_manager
669 max_w, max_h = lm.main_window.sidebar.max_tool_size()
671 alloc = self.tool.allocation
674 self.resize = event.x_root, event.y_root, w, h, \
675 min_w, min_h, max_w, max_h
678 def on_button_release_event(self, widget, event):
681 self.window.set_cursor(None)
683 def get_cursor(self, area):
684 if self.tool.floating:
685 cursor = self.floating_cursor_map.get(area)
687 cursor = self.nonfloating_cursor
690 def on_motion_notify_event(self, widget, event):
692 area = self.get_area(event.x, event.y)
693 self.window.set_cursor(self.get_cursor(area))
695 assert not self.tool.floating
696 (ptr_x_root0, ptr_y_root0, w0, h0,
697 min_w, min_h, max_w, max_h) = self.resize
698 cursor = self.nonfloating_cursor
699 self.window.set_cursor(cursor)
700 dh = event.y_root - ptr_y_root0
701 h = int(min(max(min_h, h0+dh), max_h))
702 w = -1 # constrained horizontally anyway, better to not care
703 self.tool.set_size_request(w, h)
704 self.tool.queue_resize()
706 def on_leave_notify_event(self, widget, event):
707 self.window.set_cursor(None)
710 class FoldOutArrow (gtk.Button):
712 TEXT_EXPANDED = _("Collapse")
713 TEXT_COLLAPSED = _("Expand")
715 def __init__(self, tool):
716 gtk.Button.__init__(self)
718 self.set_name("small-stock-image-button")
719 self.set_relief(gtk.RELIEF_NONE)
720 self.set_property("can-focus", False)
721 self.arrow = gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE)
722 self.set_tooltip_text(self.TEXT_EXPANDED)
724 self.connect("clicked", self.on_click)
726 def on_click(self, *a):
727 self.tool.set_rolled_up(not self.tool.rolled_up)
729 def set_arrow(self, rolled_up):
731 self.arrow.set(gtk.ARROW_RIGHT, gtk.SHADOW_NONE)
732 self.set_tooltip_text(self.TEXT_COLLAPSED)
734 self.arrow.set(gtk.ARROW_DOWN, gtk.SHADOW_NONE)
735 self.set_tooltip_text(self.TEXT_EXPANDED)
738 class ToolDragHandle (gtk.EventBox):
739 """A draggable handle for repositioning a Tool.
742 min_drag_distance = 10
745 def __init__(self, tool, label_text):
746 gtk.EventBox.__init__(self)
748 self.frame = frame = gtk.Frame()
749 frame.set_shadow_type(gtk.SHADOW_OUT)
750 self.hbox = hbox = gtk.HBox(spacing=self.spacing)
751 self.hbox.set_border_width(self.spacing)
752 self.img = gtk.Image()
753 pixbuf = getattr(self.tool.layout_manager.app.pixmaps, "tool_%s" % self.tool.role)
754 self.img.set_from_pixbuf(pixbuf)
755 hbox.pack_start(self.img, False, False)
756 self.roll_up_button = FoldOutArrow(self.tool)
757 hbox.pack_start(self.roll_up_button, False, False)
758 self.label = label = gtk.Label(label_text)
759 self.label.set_alignment(0.0, 0.5)
760 self.label.set_ellipsize(pango.ELLIPSIZE_END)
761 hbox.pack_start(label, True, True)
762 self.snap_button = SmallImageButton(tooltip=_("Snap in/out")) # TODO: update this when pressed
763 self.snap_button.connect("clicked", tool.on_snap_button_pressed)
764 self.close_button = SmallImageButton(gtk.STOCK_CLOSE, _("Close"))
765 self.close_button.connect("clicked", tool.on_close_button_pressed)
766 hbox.pack_start(self.snap_button, False, False)
767 hbox.pack_start(self.close_button, False, False)
770 self.connect("button-press-event", self.on_button_press_event)
771 self.connect("button-release-event", self.on_button_release_event)
772 self.connect("motion-notify-event", self.on_motion_notify_event)
773 self.connect("enter-notify-event", self.on_enter_notify_event)
774 self.connect("leave-notify-event", self.on_leave_notify_event)
776 self.set_events(gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK)
777 self.button_press_xy = None
778 self.in_reposition_drag = False
780 self.set_floating(False)
781 self.frame.connect("expose-event", self.on_frame_expose_event)
783 def on_frame_expose_event(self, widget, event):
784 state = self.get_state()
785 alloc = self.get_allocation()
789 # Draw a subtle vertical gradient
790 def _col2rgba(col, alpha=0.3):
791 return col.red_float, col.green_float, col.blue_float, alpha
792 light_rgba = _col2rgba(self.style.light[state])
793 mid_rgba = _col2rgba(self.style.bg[state])
794 dark_rgba = _col2rgba(self.style.dark[state])
795 cr = widget.window.cairo_create()
796 lg = cairo.LinearGradient(0, 0, 0, h)
797 lg.add_color_stop_rgba(0.0, *light_rgba)
798 lg.add_color_stop_rgba(0.5, *mid_rgba)
799 lg.add_color_stop_rgba(1.0, *dark_rgba)
803 #self.style.paint_flat_box(widget.window, state,
804 # gtk.SHADOW_OUT, event.area, widget,
805 # 'button', 0, 0, w, h)
807 return False # let the normal handler draw the frame outline too
809 def set_floating(self, floating):
811 stock_id = gtk.STOCK_GOTO_LAST
812 if self.roll_up_button in self.hbox:
813 self.roll_up_button.hide()
814 self.hbox.remove(self.roll_up_button)
816 stock_id = gtk.STOCK_GOTO_FIRST
817 if self.roll_up_button not in self.hbox:
818 self.hbox.pack_start(self.roll_up_button, False, False)
819 self.hbox.reorder_child(self.roll_up_button, 1)
820 self.roll_up_button.show_all()
822 self.snap_button.set_image_from_stock(stock_id)
824 def set_rolled_up(self, rolled_up):
825 self.roll_up_button.set_arrow(rolled_up)
827 def on_button_press_event(self, widget, event):
828 if event.button != 1:
830 if event.type == gdk._2BUTTON_PRESS:
831 if not self.tool.floating:
832 self.tool.set_rolled_up(not self.tool.rolled_up)
834 elif event.type == gdk.BUTTON_PRESS:
835 self.set_state(gtk.STATE_ACTIVE)
836 self.window.set_cursor(gdk.Cursor(gdk.FLEUR))
837 self.button_press_xy = event.x, event.y
839 def on_button_release_event(self, widget, event):
843 self.button_press_xy = None
844 self.set_state(gtk.STATE_NORMAL)
846 self.window.set_cursor(None)
848 def on_motion_notify_event(self, widget, event):
849 if not self.button_press_xy:
851 if not event.state & gdk.BUTTON1_MASK:
853 lm = self.tool.layout_manager
854 ix, iy = self.button_press_xy
855 dx, dy = event.x - ix, event.y - iy
856 dd = sqrt(dx**2 + dy**2)
857 if dd > self.min_drag_distance:
858 self.start_reposition_drag()
860 def start_reposition_drag(self): # XXX: move to Tool?
861 """Begin repositioning this tool."""
862 lm = self.tool.layout_manager
863 ix, iy = self.button_press_xy
864 self.button_press_xy = None
865 self.window.set_cursor(gdk.Cursor(gdk.FLEUR))
866 self.set_state(gtk.STATE_ACTIVE)
867 self.tool.layout_manager.drag_state.begin(self.tool, ix, iy)
869 def on_reposition_drag_finished(self): # XXX: move to Tool?
870 """Called when repositioning has finished."""
873 def on_leave_notify_event(self, widget, event):
874 self.window.set_cursor(None)
875 self.set_state(gtk.STATE_NORMAL)
877 def on_enter_notify_event(self, widget, event):
878 self.window.set_cursor(gdk.Cursor(gdk.HAND2))
879 #if not self.in_reposition_drag:
880 # self.set_state(gtk.STATE_PRELIGHT)
883 class ToolWindow (gtk.Window, ElasticContainer, WindowWithSavedPosition):
884 """Window containing a Tool in the floating state.
887 def __init__(self, title, role, layout_manager):
888 gtk.Window.__init__(self)
889 ElasticContainer.__init__(self)
890 WindowWithSavedPosition.__init__(self)
891 self.layout_manager = layout_manager
892 self.set_transient_for(layout_manager.main_window)
893 self.set_type_hint(gdk.WINDOW_TYPE_HINT_UTILITY)
894 self.set_focus_on_map(False)
895 self.set_decorated(False)
898 self.set_title(title)
900 self.connect("configure-event", self.on_configure_event)
901 self.pre_hide_pos = None
905 gtk.Window.add(self, tool)
907 def remove(self, tool):
909 gtk.Window.remove(self, tool)
911 def on_configure_event(self, widget, event):
912 if self.pre_hide_pos:
915 lm = self.layout_manager
916 if not lm.prefs.get(role, False):
918 lm.prefs[role]['floating'] = True
921 """Shows or re-shows the window.
923 If a previous position was set by hide(), it will be restored.
925 gtk.Window.show(self)
926 if self.pre_hide_pos:
927 self.move(*self.pre_hide_pos)
928 self.pre_hide_pos = None
931 """Shows or re-shows the window and all its descendents.
933 If a previous position was set by hide(), it will be restored.
935 gtk.Window.show_all(self)
936 if self.pre_hide_pos:
937 self.move(*self.pre_hide_pos)
938 self.pre_hide_pos = None
941 """Hides the window, remembering its position.
943 self.pre_hide_pos = self.get_position()
944 gtk.Window.hide(self)
946 def place(self, x, y, w, h):
947 """Places a window at a position, either now or when mapped.
949 This is intended to be called before a show() or show_all() by
950 code that lets the user pick a position for a tool window before it's
951 shown: for example when snapping a tool free from the sidebar using
954 The pre-hide position will be forgotten and not used by the override
955 for show() or show_all().
957 self.pre_hide_pos = None
959 # Constrain x and y so negative positions passed in don't look like
960 # the funky right or bottom edge positioning magic (to the realize
961 # handler; move() doesn't care, but be symmetrical in behaviour).
965 if self.window is not None:
966 # It's mapped, so this will probably work even if the window
967 # is not viewable right now.
971 # No window yet, not mapped.
972 # Update prefs ready for the superclass's realize handler.
974 lm = self.layout_manager
975 if not lm.prefs.get(role, False):
977 new_size = dict(x=x, y=y, w=w, h=h)
978 lm.prefs[role].update(new_size)
981 class Tool (gtk.VBox, ElasticContainer):
982 """Container for a dockable tool widget.
984 The widget may be packed into a Sidebar, or snapped out into its own
988 def __init__(self, widget, role, title, gloss, layout_manager):
989 gtk.VBox.__init__(self)
990 ElasticContainer.__init__(self)
992 self.layout_manager = layout_manager
993 self.handle = ToolDragHandle(self, gloss)
994 self.pack_start(self.handle, False, False)
995 self.widget_frame = frame = gtk.Frame()
996 frame.set_shadow_type(gtk.SHADOW_IN)
999 self.pack_start(frame, True, True)
1000 self.resize_grip = ToolResizeGrip(self)
1001 self.resize_grip_frame = gtk.Frame()
1002 self.resize_grip_frame.set_shadow_type(gtk.SHADOW_OUT)
1003 self.resize_grip_frame.add(self.resize_grip)
1004 self.pack_start(self.resize_grip_frame, False, False)
1005 self.floating_window = ToolWindow(title, role, layout_manager)
1006 self.floating_window.connect("delete-event", self.on_floating_window_delete_event)
1007 self.layout_manager.window_group.add_window(self.floating_window)
1008 self.floating = False
1010 self.rolled_up = False
1011 self.rolled_up_prev_size = None
1012 self.connect("size-allocate", self.on_size_allocate)
1014 def on_size_allocate(self, widget, allocation):
1017 lm = self.layout_manager
1018 if self not in lm.main_window.sidebar.tools_vbox:
1020 if not lm.prefs.get(self.role, False):
1021 lm.prefs[self.role] = {}
1022 lm.prefs[self.role]["sbheight"] = allocation.height
1024 def on_floating_window_delete_event(self, window, event):
1025 self.set_hidden(True, reason="window-deleted")
1026 return True # Suppress ordinary deletion. We'll be wanting it again.
1028 def set_show_resize_grip(self, show):
1029 widget = self.resize_grip_frame
1035 if widget not in self:
1036 self.pack_start(widget, False, False)
1037 self.reorder_child(widget, -1)
1040 def set_show_widget_frame(self, show):
1041 widget = self.widget_frame
1047 if widget not in self:
1048 self.pack_start(widget, True, True)
1049 self.reorder_child(widget, 1)
1052 def restore_sbheight(self):
1053 """Restore the height of the tool when docked."""
1054 lm = self.layout_manager
1056 sbheight = lm.prefs.get(role, {}).get("sbheight", None)
1057 if sbheight is not None:
1058 self.set_size_request(-1, sbheight)
1060 def set_floating(self, floating):
1061 """Flips the widget beween floating and non-floating, reparenting it.
1063 self.handle.set_floating(floating)
1064 self.set_rolled_up(False)
1066 # Clear any explicit size requests so that the frame is able to adopt a
1067 # natural size again.
1068 for wid in (self.handle, self):
1069 if wid.get_visible():
1070 wid.set_size_request(-1, -1)
1074 self.parent.remove(self)
1075 lm = self.layout_manager
1076 if lm.prefs.get(self.role, None) is None:
1077 lm.prefs[self.role] = {}
1079 if lm.main_window.sidebar.is_empty():
1080 lm.main_window.sidebar.show_all()
1081 sbindex = lm.prefs[self.role].get("sbindex", None)
1082 lm.main_window.sidebar.add_tool(self, index=sbindex)
1083 self.floating_window.hide()
1084 self.floating = lm.prefs[self.role]["floating"] = False
1085 self.restore_sbheight()
1086 lm.main_window.sidebar.reassign_indices()
1087 self.resize_grip_frame.set_shadow_type(gtk.SHADOW_NONE)
1089 self.floating_window.add(self)
1090 # Defer the show_all(), seems to be needed when toggling on a
1091 # hidden, floating window which hasn't yet been loaded.
1092 gobject.idle_add(self.floating_window.show_all)
1093 self.floating = lm.prefs[self.role]["floating"] = True
1094 lm.main_window.sidebar.reassign_indices()
1095 if lm.main_window.sidebar.is_empty():
1096 lm.main_window.sidebar.hide()
1097 self.resize_grip_frame.set_shadow_type(gtk.SHADOW_OUT)
1099 def set_hidden(self, hidden, reason=None, temporary=False):
1100 """Sets a tool as hidden, hiding or showing it as appropriate.
1102 Note that this does not affect whether the window is floating or not
1103 when un-hidden it will restore to the same place in the UI it was
1106 If the `temporary` argument is true, the new state will not be saved in
1109 self.set_rolled_up(False)
1111 lm = self.layout_manager
1112 if not lm.prefs.get(role, False):
1117 self.parent.remove(self)
1119 self.floating_window.hide()
1121 self.set_floating(self.floating)
1122 # Which will restore it to the correct state
1123 self.hidden = hidden
1124 lm.notify_tool_visibility_observers(role=self.role, active=not hidden,
1125 reason=reason, temporary=temporary)
1127 lm.prefs[role]["hidden"] = hidden
1128 if lm.main_window.sidebar.is_empty():
1129 lm.main_window.sidebar.hide()
1131 lm.main_window.sidebar.show_all()
1133 def set_rolled_up(self, rolled_up):
1134 resize_needed = False
1136 if not self.rolled_up:
1137 self.rolled_up = True
1138 alloc = self.get_allocation()
1139 self.rolled_up_prev_size = (alloc.width, alloc.height)
1140 self.set_show_widget_frame(False)
1141 self.set_show_resize_grip(False)
1142 newalloc = self.handle.get_allocation()
1143 self.set_size_request(newalloc.width, newalloc.height)
1144 resize_needed = True
1147 self.rolled_up = False
1148 self.set_show_widget_frame(True)
1149 self.set_show_resize_grip(True)
1150 self.set_size_request(*self.rolled_up_prev_size)
1151 resize_needed = True
1152 self.rolled_up_prev_size = None
1153 # Since other handlers call this to unroll a window before moving it
1154 # or hiding it, it's best to perform any necessary resizes now before
1155 # returning. Otherwise sizes can become screwy when the operation
1159 while gtk.events_pending():
1160 gtk.main_iteration(False)
1161 # Notify the indicator arrow
1162 self.handle.set_rolled_up(rolled_up)
1165 def on_close_button_pressed(self, window):
1166 self.set_hidden(True, reason="close-button-pressed")
1168 def on_snap_button_pressed(self, window):
1170 display = gdk.display_get_default()
1171 screen, ptr_x, ptr_y, _modmask = display.get_pointer()
1173 self.set_rolled_up(False) # pending events will be processed
1174 alloc = self.allocation
1177 titlebar_h = self.handle.allocation.height
1180 sidebar = self.layout_manager.main_window.sidebar
1181 n = sidebar.num_tools()
1183 if not self.floating:
1184 x = ptr_x - (w + (n*w/m)) # cascade effect
1185 y = ptr_y + ((m - n) * (titlebar_h + 6))
1186 self.floating_window.place(x, y, w, h)
1187 self.set_floating(True)
1188 self.handle.snap_button.set_state(gtk.STATE_NORMAL)
1189 if sidebar.is_empty():
1192 if sidebar.is_empty():
1194 self.set_floating(False)
1195 self.handle.snap_button.set_state(gtk.STATE_NORMAL)
1197 gobject.idle_add(handle_snap)
1199 def get_preview_size(self):
1200 alloc = self.get_allocation()
1201 return alloc.width, alloc.height
1203 def get_minimum_size(self):
1205 for child in self.handle, self.resize_grip_frame, self.widget_frame:
1206 child_min_w, child_min_h = child.size_request()
1207 min_w = max(min_w, child_min_w)
1208 min_h += child_min_h
1212 class ToolDragPreviewWindow (gtk.Window):
1213 """A shaped outline window showing where the current drag will move a Tool.
1216 def __init__(self, layout_manager):
1217 gtk.Window.__init__(self, gtk.WINDOW_POPUP)
1218 self.owner = layout_manager.main_window
1219 #self.set_opacity(0.5)
1220 self.set_decorated(False)
1221 self.set_position(gtk.WIN_POS_MOUSE)
1222 self.connect("map-event", self.on_map_event)
1223 self.connect("expose-event", self.on_expose_event)
1224 self.connect("configure-event", self.on_configure_event)
1226 self.set_default_size(1,1)
1229 gtk.Window.show_all(self)
1230 self.window.move_resize(0, 0, 1, 1)
1232 def on_map_event(self, window, event):
1233 owner_win = self.owner.get_toplevel()
1234 self.set_transient_for(owner_win)
1235 # The first time we're mapped, set a background pixmap.
1236 # Background pixmap, a checkerboard
1238 self.bg = gdk.Pixmap(drawable=self.window, width=2, height=2)
1239 cmap = gdk.colormap_get_system()
1240 black = cmap.alloc_color(gdk.Color(0.0, 0.0, 0.0))
1241 white = cmap.alloc_color(gdk.Color(1.0, 1.0, 1.0))
1242 self.bg.set_colormap(cmap)
1243 gc = self.bg.new_gc(black, white)
1244 self.bg.draw_rectangle(gc, True, 0, 0, 2, 2)
1245 gc = self.bg.new_gc(white, black)
1246 self.bg.draw_points(gc, [(0,0), (1,1)])
1247 self.window.set_back_pixmap(self.bg, False)
1250 def on_configure_event(self, window, event):
1256 r.union_with_rect(gdk.Rectangle(0, 0, w, s))
1257 r.union_with_rect(gdk.Rectangle(0, 0, s, h))
1258 r.union_with_rect(gdk.Rectangle(0, h-s, w, s))
1259 r.union_with_rect(gdk.Rectangle(w-s, 0, s, h))
1260 self.window.shape_combine_region(r, 0, 0)
1262 def on_expose_event(self, window, event):
1263 # Clear to the backing pixmap established earlier
1264 if self.bg is not None:
1265 self.window.begin_paint_rect(event.area)
1266 self.window.set_back_pixmap(self.bg, False)
1268 self.window.end_paint()
1271 class ToolDragState:
1273 """Manages visual state during tool repositioning.
1275 The resize grip can largely take care of itself, and deploy its own grabs.
1276 However when tools are repositioned, they get reparented between floating
1277 windows and the sidebar and a grab on either tends to become confused. The
1278 DragState object establishes whatever grabs are necessary for tools to be
1279 repositioned; all they have to do is detect when the process starts and
1283 def __init__(self, layout_manager):
1284 self.layout_manager = layout_manager
1285 self.insert_pos = None
1286 self.preview = ToolDragPreviewWindow(layout_manager)
1287 self.preview_size = None
1288 self.tool = None # the box being dragged around & previewed
1289 self.handle_pos = None
1290 self.pointer_inside_sidebar = None
1293 def connect_reposition_handlers(self):
1294 """Connect the event handlers used while repositioning tools.
1296 win = self.layout_manager.main_window
1297 conn_id = win.connect("motion-notify-event",
1298 self.on_reposition_motion_notify)
1299 self.conn_ids.append(conn_id)
1300 conn_id = win.connect("button-release-event",
1301 self.on_reposition_button_release)
1302 self.conn_ids.append(conn_id)
1304 def disconnect_reposition_handlers(self):
1305 """Disconnect the handlers set up by connect_reposition_handlers().
1307 win = self.layout_manager.main_window
1308 while self.conn_ids:
1309 conn_id = self.conn_ids.pop()
1310 win.disconnect(conn_id)
1312 def begin(self, tool, handle_x, handle_y):
1313 """Begin repositioning a tool using its drag handle.
1315 It's assumed the pointer button is held down at this point. The rest of
1316 the procedure happens behind a pointer grab on the main window, and
1317 ends when the button is released.
1319 lm = self.layout_manager
1321 # Show the preview window
1322 width, height = tool.get_preview_size()
1323 self.preview.show_all()
1324 self.preview_size = (width, height)
1325 self.handle_pos = (handle_x, handle_y)
1327 # Establish a pointer grab on the main window (which stays mapped
1328 # throughout the drag).
1329 main_win = lm.main_window
1330 main_win_gdk = main_win.window
1331 events = gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK \
1332 | gdk.BUTTON1_MOTION_MASK
1333 grab_status = gdk.pointer_grab(main_win_gdk, False, events,
1334 None, gdk.Cursor(gdk.FLEUR), 0L)
1335 if grab_status != gtk.gdk.GRAB_SUCCESS:
1336 warn("Grab failed, aborting", RuntimeWarning, 2)
1340 self.connect_reposition_handlers()
1342 gdk.pointer_ungrab()
1343 self.disconnect_reposition_handlers()
1346 def on_reposition_button_release(self, widget, event):
1347 """Ends the current tool reposition drag.
1349 gdk.pointer_ungrab()
1350 self.disconnect_reposition_handlers()
1353 def on_reposition_motion_notify(self, widget, event):
1354 """Bulk of window shuffling within a tool reposition drag.
1356 x_root = event.x_root
1357 y_root = event.y_root
1358 w, h = self.preview_size
1359 # Show the sidebar if it's not currently visible
1360 sidebar = self.layout_manager.main_window.sidebar
1361 if sidebar.is_empty():
1363 # Moving into or out of the box defined by the sidebar
1364 # changes the mode of the current drag.
1365 sb_left, sb_top = sidebar.window.get_origin()
1366 sb_alloc = sidebar.allocation
1367 sb_right = sb_left + sb_alloc.width
1368 sb_bottom = sb_top + sb_alloc.height
1369 if (x_root < sb_left or x_root > sb_right
1370 or y_root < sb_top or y_root > sb_bottom):
1371 self.pointer_inside_sidebar = False
1373 self.pointer_inside_sidebar = True
1374 # Move the preview window
1375 if self.pointer_inside_sidebar:
1377 ins = sidebar.insertion_point_at_pointer()
1378 if ins is not None and ins != self.insert_pos:
1379 self.insert_pos = ins
1380 if not tool in sidebar.tools_vbox:
1381 tool.set_floating(False)
1382 # Update preview size: the widget will be a different
1383 # size after being shoved in the sidebar.
1384 while gtk.events_pending():
1385 gtk.main_iteration(False)
1386 w, h = self.preview_size = tool.get_preview_size()
1387 sidebar.reorder_item(tool, ins)
1388 sidebar.reassign_indices()
1389 x, y = tool.handle.window.get_origin()
1391 ix, iy = self.handle_pos
1392 x = int(x_root - ix)
1393 y = int(y_root - iy)
1394 self.handle_pos_root = (x, y)
1395 self.preview.window.move_resize(x, y, w, h)
1398 """Invoked at the end of repositioning. Snapping out happens here.
1400 sidebar = self.layout_manager.main_window.sidebar
1401 if self.pointer_inside_sidebar:
1402 pass # Widget has already been reordered,
1403 # or snapped in and then reordered.
1405 # Set window position to that of the floating window
1406 x, y = self.handle_pos_root
1407 w, h = self.preview_size
1408 self.tool.floating_window.place(x, y, w, h)
1410 if self.tool in sidebar.tools_vbox:
1411 self.tool.set_floating(True)
1413 self.tool.handle.on_reposition_drag_finished()
1415 self.handle_pos = None
1416 self.insert_pos = None
1417 self.pointer_inside_sidebar = None
1418 self.preview_pos_root = (0, 0)
1419 if sidebar.is_empty():
1423 class Sidebar (gtk.EventBox):
1424 """Vertical sidebar containing reorderable tools which can be snapped out.
1429 def __init__(self, layout_manager):
1430 gtk.EventBox.__init__(self)
1431 self.layout_manager = layout_manager
1432 self.scrolledwin = gtk.ScrolledWindow()
1433 self.scrolledwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
1434 self.add(self.scrolledwin)
1435 self.tools_vbox = gtk.VBox()
1436 self.scrolledwin.add_with_viewport(self.tools_vbox)
1437 self.slack = gtk.EventBox() # needs its own window
1438 self.tools_vbox.pack_start(self.slack, True, True)
1439 self.set_size_request(self.MIN_SIZE, self.MIN_SIZE)
1441 def add_tool(self, widget, index=None):
1442 assert isinstance(widget, Tool)
1445 self.tools_vbox.pack_start(widget, expand=False, fill=False)
1446 if index is not None:
1447 self.tools_vbox.reorder_child(widget, index)
1448 self.reassign_indices()
1449 self.tools_vbox.reorder_child(self.slack, -1)
1451 def remove_tool(self, widget):
1452 assert isinstance(widget, Tool)
1453 self.reassign_indices()
1454 self.tools_vbox.remove(widget)
1458 self.reassign_indices()
1460 def reorder_item(self, item, pos):
1461 assert isinstance(item, Tool)
1462 self.tools_vbox.reorder_child(item, pos)
1463 self.tools_vbox.reorder_child(self.slack, -1)
1464 self.reassign_indices()
1466 def get_tools(self):
1468 for widget in self.tools_vbox:
1469 if widget is self.slack:
1471 tools.append(widget)
1474 def num_tools(self):
1475 """Returns the number of tools in the sidebar"""
1476 return len(self.get_tools())
1479 """True if there are no tools in the sidebar."""
1480 return self.num_tools() == 0
1482 def insertion_point_at_pointer(self):
1483 """Returns where in the sidebar a tool would be inserted.
1485 Returns an integer position for passing to reorder_item(), or None.
1486 Currently only tool drag handles are valid insertion points.
1488 window_info = gdk.window_at_pointer()
1489 if window_info is None:
1491 pointer_window = window_info[0]
1492 current_tool = self.layout_manager.drag_state.tool
1494 for widget in self.tools_vbox:
1495 if widget is self.slack:
1496 if widget.window is pointer_window:
1498 if isinstance(widget, Tool):
1499 if widget is not current_tool:
1500 dragger = widget.handle
1501 if dragger.window is pointer_window:
1506 def max_tool_size(self):
1507 """Returns the largest a packed tool is allowed to be.
1509 scrwin = self.scrolledwin
1510 viewpt = scrwin.get_child()
1511 sb_pad = 2 * scrwin.style_get_property("scrollbar-spacing")
1512 vp_alloc = viewpt.allocation
1513 max_size = (vp_alloc.width-sb_pad, vp_alloc.height-sb_pad)
1516 def reassign_indices(self):
1517 """Calculates and reassigns the "sbindex" prefs value.
1519 lm = self.layout_manager
1521 for tool in [t for t in self.tools_vbox if t is not self.slack]:
1523 if not lm.prefs.get(role, False):
1525 lm.prefs[role]["sbindex"] = i
1528 def set_tool_widths(self, width):
1529 """Constrain all packed tools' widths to a certain size.
1531 lm = self.layout_manager
1532 max_w, max_h = self.max_tool_size()
1533 for tool in [t for t in self.tools_vbox if t is not self.slack]:
1534 natural_w, natural_h = tool.size_request()
1535 req_w, req_h = tool.get_size_request()
1537 # Only constrain if the natual width is larger than permitted
1538 if natural_w > max_w:
1539 tool.set_size_request(max_w, req_h)
1542 if natural_w <= max_w:
1543 tool.set_size_request(-1, req_h)
1544 # Dubious. Could this lead to an infinite loop?
1546 tool.set_size_request(max_w, req_h)
1549 def set_initial_window_position(win, pos):
1550 """Set the position of a gtk.Window, used during initial positioning.
1552 This is used both for restoring a saved window position, and for the
1553 application-wide defaults. The ``pos`` argument is a dict containing the
1554 following optional keys
1558 If positive, the size of the window.
1559 If negative, size is calculated based on the size of the
1560 monitor with the pointer on it, and x (or y) if given, e.g.
1562 width = mouse_mon_w - abs(x) + abs(w) # or (if no x)
1563 width = mouse_mon_w - (2 * abs(w))
1565 The same is true of calulated heights.
1569 If positive, the left/top of the window.
1570 If negative, the bottom/right of the window: you MUST provide
1571 a positive w,h if you do this!
1573 If the window's calculated top-left would place it offscreen, it will be
1574 placed in its default, window manager provided position. If its calculated
1575 size is larger than the screen, the window will be given its natural size
1578 Returns the final, chosen (x, y) pair for forcing the window position on
1579 first map, or None if defaults are being used.
1582 MIN_USABLE_SIZE = 100
1584 # Final calculated positions
1585 final_x, final_y = None, None
1586 final_w, final_h = None, None
1588 # Positioning arguments
1589 x = pos.get("x", None)
1590 y = pos.get("y", None)
1591 w = pos.get("w", None)
1592 h = pos.get("h", None)
1594 # Where the mouse is right now
1595 display = gdk.display_get_default()
1596 screen, ptr_x, ptr_y, _modmask = display.get_pointer()
1598 raise RuntimeError, "No cursor on the default screen. Eek."
1599 screen_w = screen.get_width()
1600 screen_h = screen.get_height()
1601 assert screen_w > MIN_USABLE_SIZE
1602 assert screen_h > MIN_USABLE_SIZE
1604 if x is not None and y is not None:
1608 assert w is not None
1610 final_x = screen_w - w - abs(x)
1614 assert h is not None
1616 final_y = screen_h - h - abs(y)
1617 if final_x < 0 or final_x > screen_w - MIN_USABLE_SIZE: final_x = None
1618 if final_y < 0 or final_y > screen_h - MIN_USABLE_SIZE: final_h = None
1620 if w is not None and h is not None:
1624 mon_num = screen.get_monitor_at_point(ptr_x, ptr_y)
1625 mon_geom = screen.get_monitor_geometry(mon_num)
1628 final_w = max(0, mon_geom.width - abs(x) - abs(w))
1630 final_w = max(0, mon_geom.width - 2*abs(w))
1633 final_h = max(0, mon_geom.height - abs(y) - abs(h))
1635 final_h = max(0, mon_geom.height - 2*abs(h))
1636 if final_w > screen_w or final_w < MIN_USABLE_SIZE: final_w = None
1637 if final_h > screen_h or final_h < MIN_USABLE_SIZE: final_h = None
1639 # TODO: check that the final position makes sense for the current
1640 # screen (or monitor)
1642 if None not in (final_w, final_h, final_x, final_y):
1643 geom_str = "%dx%d+%d+%d" % (final_w, final_h, final_x, final_y)
1644 win.parse_geometry(geom_str)
1645 return final_x, final_y
1647 if final_w and final_h:
1648 win.set_default_size(final_w, final_h)
1649 if final_x and final_y:
1650 win.move(final_x, final_y)
1651 return final_x, final_y