# Copyright (C) 2010 Canonical # # Authors: # Jacob Johan Edwards # # 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; version 3. # # 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import logging from gi.repository import Gtk, Gdk, GLib LOG = logging.getLogger(__name__) class ActionBar(Gtk.HBox): """ This is a gtk box wrapped so that all child widgets, save one, align to the right. The widget does not show by default when its parent calls show_all(), but rather autoshows and autohides when buttons (or a left-aligned label) are added and removed. The widget is only intended to hold buttons and a label, for which it has specific methods; general add and pack are not implemented. See: https://wiki.ubuntu.com/SoftwareCenter#Main%20window https://wiki.ubuntu.com/SoftwareCenter#Custom%20package%20lists https://wiki.ubuntu.com/SoftwareCenter#software-list-view-disclosure https://wiki.ubuntu.com/SoftwareCenter#Learning_how_to_launch_an_application """ PADDING = 4 ANIMATE_START_DELAY = 50 ANIMATE_STEP_INTERVAL = 10 ANIMATE_STEP = 8 def __init__(self): super(ActionBar, self).__init__(spacing=self.PADDING) self._btns = Gtk.HBox(spacing=self.PADDING) self._btns.set_border_width(self.PADDING) self._label_hbox = Gtk.HBox() self._label_hbox.set_border_width(self.PADDING) # So that all buttons children right align self._btn_bin = Gtk.Alignment.new(1.0, 0.0, 1.0, 1.0) self._btn_bin.set_padding(0, 0, 0, 10) self._btn_bin.add(self._btns) # Buttons go on the right, labels on the left (in LTR mode) super(ActionBar, self).pack_start(self._label_hbox, False, False, 10) super(ActionBar, self).pack_end(self._btn_bin, False, True, 0) # add a CSS class so we can be themed context = self.get_style_context() context.add_class("action-bar") # Don't show_all() by default. self.set_no_show_all(True) self._label_hbox.show_all() self._btn_bin.show_all() self._visible = False # listen for size allocation events, used for implementing the # action bar slide in/out animation effect self.connect('size-allocate', self._on_size_allocate) self._is_sliding_in = False self._is_sliding_out = False self._target_height = None self.connect("draw", self._on_draw) def add_button(self, id, label, result, *result_args): """Adds a button and shows the bar. Keyword arguments: id -- A unique identifier for the button label -- A string for the button label result -- A function to be called on button click result_args -- Any arguments for the result function """ overwrite = self.get_button(id) if overwrite: self._btns.remove(overwrite) btn = Gtk.Button(label=label) btn.connect("clicked", self._callback(result, result_args)) btn.id = id btn.show() self._btns.pack_start(btn, False, True, 0) if not self._visible: # always animate with buttons self._show(animate=True) def remove_button(self, id): """Removes a button. Hides bar if no buttons left.""" children = self._btns.get_children() for child in children: if child.id == id: # lock the height of the action bar when removing buttons to # prevent an unsightly resize if a label remains but all # buttons are removed self.set_size_request(-1, self.get_allocation().height) self._btns.remove(child) if len(children) == 1 and not len(self._label_hbox): # always animate with buttons self._hide(animate=True) return def set_label(self, text, link_result=None, *link_result_args): """ Places a string on the left and shows the actionbar. Note the "_" symbol acts to delimit sections treated as a link. For example, in text="Lorem _ ipsum_ dolor _sit amat", clicking the section " ipsum" or "sit amat" will trigger the link_result. Keyword arguments: text -- a string optionally with "_" to indicate a link button. link_result -- A function to be called on link click link_result_args -- Any arguments for the result function """ sections = text.split("_") LOG.debug("got sections '%s'" % sections) self._label_text = text for i, text_for_label in enumerate(sections): action_bar_item = Gtk.Label.new(text_for_label) # every second item in the bar is a clickable link, # this is because the text.split("_") earlier (all links # are put into "_foo_" # FIXME: actually remove the "_" and use proper " # in the label, but this requires a string change # so we need to do it after 12.04 if not i % 2: markup = text_for_label action_bar_item.set_markup(markup) else: markup = '%s' % text_for_label action_bar_item.set_markup(markup) action_bar_item.connect("activate-link", self._callback(link_result, link_result_args)) self._label_hbox.pack_start(action_bar_item, True, True, 0) self._label_hbox.show_all() self._show(animate=False) def unset_label(self): """ Removes the currently set label, hiding the actionbar if no buttons are displayed. """ self._label_text = "" # Destroy all event boxes holding text segments. while len(self._label_hbox): last = self._label_hbox.get_children()[-1] self._label_hbox.remove(last) window = self.get_window() if window: window.set_cursor(None) # Then hide if there's nothing else visible. if not len(self._btns): self._hide(animate=False) def get_button(self, id): """Returns a button, or None if `id` links to no button.""" for child in self._btns.get_children(): if child.id == id: return child def clear(self): """Removes all contents and hides the bar.""" animate = len(self._btns.get_children()) > 0 self._hide(animate) for child in self._btns.get_children(): self._btns.remove(child) self.unset_label() # The following Gtk.Container methods are deimplemented to prevent # overwriting of the _elems children box, and because the only # intended contents of an ActionBar are buttons and perhaps a text # label. def add(self, widget): """Not Implemented: Do not call.""" raise NotImplementedError def add_with_properties(self, widget): """Not Implemented: Do not call.""" raise NotImplementedError def remove(self, widget): """Not Implemented: Do not call.""" raise NotImplementedError def pack_start(self, *args): """Not Implemented: Do not call.""" raise NotImplementedError def pack_default_start(self, *args): """Not Implemented: Do not call.""" raise NotImplementedError def pack_end(self, *args): """Not Implemented: Do not call.""" raise NotImplementedError def pack_default_end(self, *args): """Not Implemented: Do not call.""" raise NotImplementedError # Internal methods def _show(self, animate): if self._visible or self._is_sliding_in: return self._visible = True if animate: self._slide_in() else: super(ActionBar, self).show() def _hide(self, animate): if not self._visible or self._is_sliding_out: return if animate: self._slide_out() else: self.set_size_request(-1, -1) self._visible = False super(ActionBar, self).hide() def _slide_in(self): self._is_sliding_in = True self._target_height = self.get_size_request()[1] self._current_height = 0 self.set_size_request(-1, self._current_height) super(ActionBar, self).show() GLib.timeout_add(self.ANIMATE_START_DELAY, self._slide_in_cb) def _slide_out(self): self._is_sliding_out = True self._target_height = 0 self._current_height = self.get_size_request()[1] GLib.timeout_add(self.ANIMATE_START_DELAY, self._slide_out_cb) def _slide_in_cb(self): if (self._is_sliding_in and self._current_height < self._target_height): new_height = self._current_height + self.ANIMATE_STEP if new_height > self._target_height: new_height = self._target_height self.set_size_request(-1, new_height) else: self._is_sliding_in = False def _slide_out_cb(self): if (self._is_sliding_out and self._current_height > self._target_height): new_height = self._current_height - self.ANIMATE_STEP if new_height <= self._target_height: new_height = self._target_height self._is_sliding_out = False self.set_size_request(-1, -1) self._visible = False super(ActionBar, self).hide() else: self.set_size_request(-1, new_height) def _on_size_allocate(self, widget, allocation): # FIXME: mvo: allocation.height is no longer accessible with gtk3 # THIS SEEMS TO BREAK THE ANIMATION #height = allocation.height height = widget.get_allocation().height if self._is_sliding_in: self._current_height = height GLib.timeout_add(self.ANIMATE_STEP_INTERVAL, self._slide_in_cb, priority=100) elif self._is_sliding_out: self._current_height = height GLib.timeout_add(self.ANIMATE_STEP_INTERVAL, self._slide_out_cb, priority=100) else: self.queue_draw() def _on_draw(self, widget, cr): a = widget.get_allocation() context = widget.get_style_context() color = context.get_background_color(widget.get_state_flags()) cr.set_source_rgb(color.red, color.green, color.blue) cr.paint() color = context.get_border_color(widget.get_state_flags()) cr.set_source_rgba(color.red, color.green, color.blue, 0.5) cr.set_line_width(1) cr.move_to(0.5, 0.5) cr.rel_line_to(a.width - 1, 0) cr.stroke() def _callback(self, function, args): # Disposes of the 'widget' argument that # Gtk.Widget.connect() prepends to calls. callback = lambda *trash_args: function(*args) return callback def _hover_link(self, linkbox, *args): # Makes the link in given eventbox bold # label = linkbox.get_child() # label.set_markup("%s" % label.get_text()) window = self.get_window() window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.HAND2)) def _unhover_link(self, linkbox, *args): # Sets the link in given eventbox to its base state # label = linkbox.get_child() # label.set_markup("%s" % label.get_text()) window = self.get_window() window.set_cursor(None) if __name__ == "__main__": win = Gtk.Window() win.set_size_request(600, 400) box = Gtk.VBox() control_box = Gtk.HBox() tree = Gtk.TreeView() bar = ActionBar() btns = 0 def perform(btn): print("Clicked 'Perform %i'" % btn) def perform_lbl(): print("Clicked label link") return True def add_func(*args): global btns btns += 1 bar.add_button(btns, "Perform %i action" % btns, perform, btns) def rmv_func(*args): global btns if btns > 0: bar.remove_button(btns) btns -= 1 def set_func(*args): # example with multiple links bar.set_label("This label _has a link_ and _another one_", perform_lbl) def unset_func(*args): bar.unset_label() addbtn = Gtk.Button(label="Add Button") rmvbtn = Gtk.Button(label="Remove Button") setbtn = Gtk.Button(label="Set Label") unsetbtn = Gtk.Button(label="Unset Label") addbtn.connect("clicked", add_func) rmvbtn.connect("clicked", rmv_func) setbtn.connect("clicked", set_func) unsetbtn.connect("clicked", unset_func) control_box.pack_start(addbtn, True, True, 0) control_box.pack_start(rmvbtn, True, True, 0) control_box.pack_start(setbtn, True, True, 0) control_box.pack_start(unsetbtn, True, True, 0) win.add(box) box.pack_start(control_box, False, True, 0) box.pack_start(tree, True, True, 0) box.pack_start(bar, False, True, 5) win.connect("destroy", Gtk.main_quit) win.show_all() Gtk.main()