diff --git a/pyfpdb/TreeViewTooltips.py b/pyfpdb/TreeViewTooltips.py new file mode 100644 index 00000000..1112d3e4 --- /dev/null +++ b/pyfpdb/TreeViewTooltips.py @@ -0,0 +1,423 @@ +# Copyright (c) 2006, Daniel J. Popowich +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Send bug reports and contributions to: +# +# dpopowich AT astro dot umass dot edu +# + +''' +TreeViewTooltips.py + +Provides TreeViewTooltips, a class which presents tooltips for cells, +columns and rows in a gtk.TreeView. + +------------------------------------------------------------ + This file includes a demo. Just execute the file: + + python TreeViewTooltips.py +------------------------------------------------------------ + +To use, first subclass TreeViewTooltips and implement the get_tooltip() +method; see below. Then add any number of gtk.TreeVew widgets to a +TreeViewTooltips instance by calling the add_view() method. Overview +of the steps: + + # 1. subclass TreeViewTooltips + class MyTooltips(TreeViewTooltips): + + # 2. overriding get_tooltip() + def get_tooltip(...): + ... + + # 3. create an instance + mytips = MyTooltips() + + # 4. Build up your gtk.TreeView. + myview = gtk.TreeView() + ...# create columns, set the model, etc. + + # 5. Add the view to the tooltips + mytips.add_view(myview) + +How it works: the add_view() method connects the TreeView to the +"motion-notify" event with the callback set to a private method. +Whenever the mouse moves across the TreeView the callback will call +get_tooltip() with the following arguments: + + get_tooltip(view, column, path) + +where, + + view: the gtk.TreeView instance. + column: the gtk.TreeViewColumn instance that the mouse is + currently over. + path: the path to the row that the mouse is currently over. + +Based on whether or not column and path are checked for specific +values, get_tooltip can return tooltips for a cell, column, row or the +whole view: + + Column Checked Path Checked Tooltip For... + Y Y cell + Y N column + N Y row + N N view + +get_tooltip() should return None if no tooltip should be displayed. +Otherwise the return value will be coerced to a string (with the str() +builtin) and stripped; if non-empty, the result will be displayed as +the tooltip. By default, the tooltip popup window will be displayed +centered and just below the pointer and will remain shown until the +pointer leaves the cell (or column, or row, or view, depending on how +get_tooltip() is implemented). + +''' + + +import pygtk +pygtk.require('2.0') + +import gtk +import gtk.gdk +import gobject + +if gtk.gtk_version < (2, 8): + import warnings + + msg = ('''This module was developed and tested with version 2.8.18 of gtk. You are using version %d.%d.%d. Your milage may vary.''' + % gtk.gtk_version) + warnings.warn(msg) + + +# major, minor, patch +version = 1, 0, 0 + +class TreeViewTooltips: + + def __init__(self): + + ''' + Initialize the tooltip. After initialization there are two + attributes available for advanced control: + + window: the popup window that holds the tooltip text, an + instance of gtk.Window. + label: a gtk.Label that is packed into the window. The + tooltip text is set in the label with the + set_label() method, so the text can be plain or + markup text. + + Be default, the tooltip is enabled. See the enabled/disabled + methods. + ''' + + # create the window + self.window = window = gtk.Window(gtk.WINDOW_POPUP) + window.set_name('gtk-tooltips') + window.set_resizable(False) + window.set_border_width(4) + window.set_app_paintable(True) + window.connect("expose-event", self.__on_expose_event) + + + # create the label + self.label = label = gtk.Label() + label.set_line_wrap(True) + label.set_alignment(0.5, 0.5) + label.set_use_markup(True) + label.show() + window.add(label) + + # by default, the tooltip is enabled + self.__enabled = True + # saves the current cell + self.__save = None + # the timer id for the next tooltip to be shown + self.__next = None + # flag on whether the tooltip window is shown + self.__shown = False + + def enable(self): + 'Enable the tooltip' + + self.__enabled = True + + def disable(self): + 'Disable the tooltip' + + self.__enabled = False + + def __show(self, tooltip, x, y): + + '''show the tooltip popup with the text/markup given by + tooltip. + + tooltip: the text/markup for the tooltip. + x, y: the coord. (root window based) of the pointer. + ''' + + window = self.window + + # set label + self.label.set_label(tooltip) + # resize window + w, h = window.size_request() + # move the window + window.move(*self.location(x,y,w,h)) + # show it + window.show() + self.__shown = True + + def __hide(self): + 'hide the tooltip' + + self.__queue_next() + self.window.hide() + self.__shown = False + + def __leave_handler(self, view, event): + 'when the pointer leaves the view, hide the tooltip' + + self.__hide() + + def __motion_handler(self, view, event): + 'As the pointer moves across the view, show a tooltip.' + + path = view.get_path_at_pos(int(event.x), int(event.y)) + + if self.__enabled and path: + path, col, x, y = path + tooltip = self.get_tooltip(view, col, path) + if tooltip is not None: + tooltip = str(tooltip).strip() + if tooltip: + self.__queue_next((path, col), tooltip, + int(event.x_root), + int(event.y_root)) + return + + self.__hide() + + def __queue_next(self, *args): + + 'queue next request to show a tooltip' + + # if args is non-empty it means a request was made to show a + # tooltip. if empty, no request is being made, but any + # pending requests should be cancelled anyway. + + cell = None + + # if called with args, break them out + if args: + cell, tooltip, x, y = args + + # if it's the same cell as previously shown, just return + if self.__save == cell: + return + + # if we have something queued up, cancel it + if self.__next: + gobject.source_remove(self.__next) + self.__next = None + + # if there was a request... + if cell: + # if the tooltip is already shown, show the new one + # immediately + if self.__shown: + self.__show(tooltip, x, y) + # else queue it up in 1/2 second + else: + self.__next = gobject.timeout_add(500, self.__show, + tooltip, x, y) + + # save this cell + self.__save = cell + + + def __on_expose_event(self, window, event): + + # this magic is required so the window appears with a 1-pixel + # black border (default gtk Style). This code is a + # transliteration of the C implementation of gtk.Tooltips. + w, h = window.size_request() + window.style.paint_flat_box(window.window, gtk.STATE_NORMAL, + gtk.SHADOW_OUT, None, window, + 'tooltip', 0, 0, w, h) + + def location(self, x, y, w, h): + + '''Given the x,y coordinates of the pointer and the width and + height (w,h) demensions of the tooltip window, return the x, y + coordinates of the tooltip window. + + The default location is to center the window on the pointer + and 4 pixels below it. + ''' + + return x - w/2, y + 4 + + def add_view(self, view): + + 'add a gtk.TreeView to the tooltip' + + assert isinstance(view, gtk.TreeView), \ + ('This handler should only be connected to ' + 'instances of gtk.TreeView') + + view.connect("motion-notify-event", self.__motion_handler) + view.connect("leave-notify-event", self.__leave_handler) + + def get_tooltip(self, view, column, path): + 'See the module doc string for a description of this method' + + raise NotImplemented, 'Subclass must implement get_tooltip()' + + +if __name__ == '__main__': + + ############################################################ + # DEMO + ############################################################ + + # First, subclass TreeViewTooltips + + class DemoTips(TreeViewTooltips): + + def __init__(self, customer_column): + # customer_column is an instance of gtk.TreeViewColumn and + # is being used in the gtk.TreeView to show customer names. + self.cust_col = customer_column + + # call base class init + TreeViewTooltips.__init__(self) + + def get_tooltip(self, view, column, path): + + # we have a two column view: customer, phone; we'll make + # tooltips cell-based for the customer column, but generic + # column-based for the phone column. + + # customer + if column is self.cust_col: + + # By checking both column and path we have a + # cell-based tooltip. + model = view.get_model() + customer = model[path][2] + return '%s %s\n%s' % (customer.fname, + customer.lname, + customer.notes) + # phone + else: + return ('Generic Column Tooltip\n' + 'Unless otherwise noted, all\narea codes are 888') + + def XX_location(self, x, y, w, h): + # rename me to "location" so I override the base class + # method. This will demonstrate being able to change + # where the tooltip window popups, relative to the + # pointer. + + # this will place the tooltip above and to the right + return x + 10, y - (h + 10) + + # Here's our customer + class Customer: + + def __init__(self, fname, lname, phone, notes): + self.fname = fname + self.lname = lname + self.phone = phone + self.notes = notes + + # create a bunch of customers + customers = [] + for fname, lname, phone, notes in [ + ('Joe', 'Schmoe', '555-1212', 'Likes to Morris dance.'), + ('Jane', 'Doe', '555-2323', + 'Wonders what the hell\nMorris dancing is.'), + ('Phred', 'Phantastic', '900-555-1212', 'Dreams of Betty.'), + ('Betty', 'Boop', '555-3434', 'Dreams in b&w.'), + ('Red Sox', 'Fan', '555-4545', + "Still livin' 2004!\nEspecially after 2006.")]: + customers.append(Customer(fname, lname, phone, notes)) + + # Build our model and view + model = gtk.ListStore(str, str, object) + for c in customers: + model.append(['%s %s' % (c.fname, c.lname), c.phone, c]) + + view = gtk.TreeView(model) + view.get_selection().set_mode(gtk.SELECTION_NONE) + + # two columns, name and phone + cell = gtk.CellRendererText() + cell.set_property('xpad', 20) + namecol = gtk.TreeViewColumn('Customer Name', cell, text=0) + namecol.set_min_width(200) + view.append_column(namecol) + + cell = gtk.CellRendererText() + phonecol = gtk.TreeViewColumn('Phone', cell, text=1) + view.append_column(phonecol) + + # finally, connect the tooltip, specifying the name column as the + # column we want the tooltip to popup over. + tips = DemoTips(namecol) + tips.add_view(view) + + # We're going to demonstrate enable/disable. First we need a + # callback function to connect to the toggled signal. + def toggle(button): + if button.get_active(): + tips.disable() + else: + tips.enable() + + # create a checkbutton and connect our handler + check = gtk.CheckButton('Check to disable view tooltips') + check.connect('toggled', toggle) + + # a standard gtk.Tooltips to compare to + tt = gtk.Tooltips() + tt.set_tip(check, ('This is a standard gtk tooltip.\n' + 'Compare me to the tooltips above.')) + + # create a VBox to pack the view and checkbutton + vbox = gtk.VBox() + vbox.pack_start(view) + vbox.pack_start(check, False) + vbox.show_all() + + # pack the vbox into a simple dialog and run it + dialog = gtk.Dialog('TreeViewTooltips Demo') + close = dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_NONE) + + # add a tooltip for the close button + tt.set_tip(close, 'Click to end the demo.') + + dialog.set_default_size(400,400) + dialog.vbox.pack_start(vbox) + dialog.run()