Refactored internal shapes code not completely backward compatible
Looks like #519 is not completely backwards compatible. I have a plugin that uses drawn shape to create masked data for calculation, which failed with this error now:

```python
2017-07-03 15:26:36,348 | E | Callback.py:139 (make_callback) |
  Error making callback 'draw-event': setting an array element with a sequence.
2017-07-03 15:26:36,348 | E | Callback.py:140 (make_callback) | Traceback:
  File ".../ginga/misc/Callback.py", line 123, in make_callback
    res = method(*cb_args, **cb_kwdargs)

  File ".../stginga/plugins/BadPixCorr.py", line 371, in draw_cb
    return self.redo()

  File ".../stginga/plugins/BadPixCorr.py", line 234, in redo
    sci_masked = image.cutout_shape(sig_obj)

  File ".../ginga/BaseImage.py", line 388, in cutout_shape
    view, mask = self.get_shape_view(shape_obj)

  File ".../ginga/BaseImage.py", line 376, in get_shape_view
    contains = shape_obj.contains_arr(xi, yi)

  File ".../ginga/canvas/CanvasObject.py", line 551, in contains_arr
    return self.contains_pts(pts)

  File ".../ginga/canvas/types/astro.py", line 512, in contains_pts
    arg1 = obj2.contains_pts(pts)
```
diff --git a/ginga/GingaPlugin.py b/ginga/GingaPlugin.py
index 75c4f843..64422aaf 100644
--- a/ginga/GingaPlugin.py
+++ b/ginga/GingaPlugin.py
@@ -71,6 +71,10 @@ class GlobalPlugin(BasePlugin):
         """This method is called when an image is set in a channel."""
         pass
 
+    def blank(self, channel):
+        """This method is called when a channel is no longer displaying any object."""
+        pass
+
 
 class LocalPlugin(BasePlugin):
     """Class to handle a local plugin."""
@@ -85,6 +89,8 @@ class LocalPlugin(BasePlugin):
             # TO BE DEPRECATED
             self.chinfo = self.channel
 
+            self.fv.add_callback('delete-channel', self._delete_channel_cb)
+
     def modes_off(self):
         """Turn off any mode user may be in."""
         bm = self.fitsimage.get_bindmap()
@@ -121,4 +127,17 @@ class LocalPlugin(BasePlugin):
         """
         pass
 
+    def blank(self):
+        """
+        This method is called when no object is displayed in the channel
+        associated with the plugin.  It can optionally clear whatever operation
+        it is doing.
+        """
+        pass
+
+    def _delete_channel_cb(self, fv, channel):
+        # stop ourself if our channel is deleted
+        if channel is self.channel:
+            self.stop()
+
 # END
diff --git a/ginga/canvas/types/astro.py b/ginga/canvas/types/astro.py
index c0d62fa5..bf94f2ef 100644
--- a/ginga/canvas/types/astro.py
+++ b/ginga/canvas/types/astro.py
@@ -161,6 +161,10 @@ class Ruler(TwoPointMixin, CanvasObjectBase):
         return (text_x, text_y, text_h)
 
     def draw(self, viewer):
+        image = viewer.get_image()
+        if image is None:
+            return
+
         points = self.get_points()
         x1, y1 = points[0]
         x2, y2 = points[1]
@@ -329,6 +333,10 @@ class Compass(OnePointOneRadiusMixin, CanvasObjectBase):
         return self.within_radius(viewer, pt, p0, self.cap_radius)
 
     def draw(self, viewer):
+        image = viewer.get_image()
+        if image is None:
+            return
+
         cr = viewer.renderer.setup_cr(self)
         cr.set_font_from_shape(self)
 
@@ -475,6 +483,8 @@ class Crosshair(OnePointMixin, CanvasObjectBase):
 
             else:
                 image = viewer.get_image()
+                if image is None:
+                    return
                 # NOTE: x, y are assumed to be in data coordinates
                 info = image.info_xy(self.x, self.y, viewer.get_settings())
                 if self.format == 'coords':
diff --git a/ginga/examples/configs/plugin_Thumbs.cfg b/ginga/examples/configs/plugin_Thumbs.cfg
index 363beeed..77a15367 100644
--- a/ginga/examples/configs/plugin_Thumbs.cfg
+++ b/ginga/examples/configs/plugin_Thumbs.cfg
@@ -28,6 +28,9 @@ rebuild_wait = 4.0
 # Max length of thumb on the long side
 thumb_length = 150
 
+# Separation between thumbs in pixels
+thumb_sep = 15
+
 # Sort the thumbs alphabetically
 #sort_order = 'alpha'
 sort_order = None
@@ -35,7 +38,7 @@ sort_order = None
 # Thumbnail label length in num of characters (None = no limit)
 label_length = 25
 
-# Cut off long label ('left' or 'right')
+# Cut off long label ('left', 'right' or None)
 label_cutoff = 'right'
 
 # Option to highlight images that are displayed in channels.
diff --git a/ginga/rv/Channel.py b/ginga/rv/Channel.py
index 74b96cff..a59f078e 100644
--- a/ginga/rv/Channel.py
+++ b/ginga/rv/Channel.py
@@ -38,7 +38,6 @@ class Channel(Callback.Callbacks):
         self.fv = fv
         self.settings = settings
         self.logger = fv.logger
-        self.lock = threading.RLock()
 
         # CHANNEL ATTRIBUTES
         self.name = name
@@ -63,7 +62,7 @@ class Channel(Callback.Callbacks):
         self.extdata = Bunch.Bunch()
 
         self._configure_sort()
-        self.settings.getSetting('sort_order').add_callback(
+        self.settings.get_setting('sort_order').add_callback(
             'set', self._sort_changed_ext_cb)
 
     def connect_viewer(self, viewer):
@@ -82,25 +81,43 @@ class Channel(Callback.Callbacks):
         if self == channel:
             return
 
+        if imname in channel:
+            # image with that name is already there
+            return
+
+        # transfer image info
+        info = self.image_index[imname]
+        was_not_there_already = channel._add_info(info)
+
         try:
-            # copy image to other channel's datasrc if still
-            # in memory
             image = self.datasrc[imname]
 
         except KeyError:
-            # transfer image info
-            info = self.image_index[imname]
-            channel._add_info(info)
             return
 
-        #channel.datasrc[imname] = image
-        channel.add_image(image, silent=silent)
+        if was_not_there_already:
+            channel.datasrc[imname] = image
+            image.add_callback('modified', channel._image_modified_cb)
+
+            if not silent:
+                self.fv.gui_do(channel._add_image_update, image, info)
 
     def remove_image(self, imname):
-        if self.datasrc.has_key(imname):
+        info = self.image_index[imname]
+        self.remove_history(imname)
+
+        if imname in self.datasrc:
+            image = self.datasrc[imname]
             self.datasrc.remove(imname)
 
-        info = self.remove_history(imname)
+            # update viewer if we are removing the currently displayed image
+            cur_image = self.viewer.get_image()
+            if cur_image == image:
+                self.refresh_cursor_image()
+
+        self.fv.make_async_gui_callback('remove-image', self.name,
+                                        info.name, info.path)
+
         return info
 
     def get_image_names(self):
@@ -139,14 +156,19 @@ class Channel(Callback.Callbacks):
 
         self.datasrc[imname] = image
 
-        idx = image.get('idx', None)
-        path = image.get('path', None)
-        image_loader = image.get('image_loader', None)
-        image_future = image.get('image_future', None)
-        info = self.add_history(imname, path,
-                                image_loader=image_loader,
-                                image_future=image_future,
-                                idx=idx)
+        # Has this image been loaded into a channel before?
+        info = image.get('image_info', None)
+        if info is None:
+            # No
+            idx = image.get('idx', None)
+            path = image.get('path', None)
+            image_loader = image.get('image_loader', None)
+            image_future = image.get('image_future', None)
+            info = self.add_history(imname, path,
+                                    image_loader=image_loader,
+                                    image_future=image_future,
+                                    idx=idx)
+            image.set(image_info=info)
 
         # we'll get notified if an image changes and can update
         # metadata and make a chained callback
@@ -176,7 +198,6 @@ class Channel(Callback.Callbacks):
         info = self.add_history(info.name, info.path,
                                 image_loader=image_loader,
                                 image_future=image_future)
-        self.fv.make_async_gui_callback('add-image-info', self, info)
 
     def get_image_info(self, imname):
         return self.image_index[imname]
@@ -201,7 +222,11 @@ class Channel(Callback.Callbacks):
                 self.fv.change_channel(self.name)
 
     def _image_modified_cb(self, image):
-        imname = image.get('name')
+        imname = image.get('name', None)
+        if (imname is None) or (imname not in self.image_index):
+            # not one of ours apparently (maybe used to be, but got removed)
+            return
+
         info = self.image_index[imname]
         info.time_modified = datetime.utcnow()
         self.logger.debug("image modified; making chained callback")
@@ -209,6 +234,11 @@ class Channel(Callback.Callbacks):
         self.fv.make_async_gui_callback('add-image-info', self, info)
 
     def refresh_cursor_image(self):
+        if self.cursor < 0:
+            self.viewer.clear()
+            self.fv.channel_image_updated(self, None)
+            return
+
         info = self.history[self.cursor]
         if self.datasrc.has_key(info.name):
             # image still in memory
@@ -219,44 +249,50 @@ class Channel(Callback.Callbacks):
             self.switch_name(info.name)
 
     def prev_image(self, loop=True):
-        with self.lock:
-            self.logger.debug("Previous image")
-            if self.cursor <= 0:
-                n = len(self.history) - 1
-                if (not loop) or (n < 0):
-                    self.logger.error("No previous image!")
-                    return True
-                self.cursor = n
-            else:
-                self.cursor -= 1
+        self.logger.debug("Previous image")
+        if self.cursor <= 0:
+            n = len(self.history) - 1
+            if (not loop) or (n < 0):
+                self.logger.error("No previous image!")
+                return True
+            self.cursor = n
+        else:
+            self.cursor -= 1
 
-            self.refresh_cursor_image()
+        self.refresh_cursor_image()
 
         return True
 
     def next_image(self, loop=True):
-        with self.lock:
-            self.logger.debug("Next image")
-            n = len(self.history) - 1
-            if self.cursor >= n:
-                if (not loop) or (n < 0):
-                    self.logger.error("No next image!")
-                    return True
-                self.cursor = 0
-            else:
-                self.cursor += 1
+        self.logger.debug("Next image")
+        n = len(self.history) - 1
+        if self.cursor >= n:
+            if (not loop) or (n < 0):
+                self.logger.error("No next image!")
+                return True
+            self.cursor = 0
+        else:
+            self.cursor += 1
 
-            self.refresh_cursor_image()
+        self.refresh_cursor_image()
 
         return True
 
     def _add_info(self, info):
-        if not info in self.image_index:
-            self.history.append(info)
-            self.image_index[info.name] = info
+        if info in self.image_index:
+            # image info is already present
+            return False
+
+        self.history.append(info)
+        self.image_index[info.name] = info
+
+        if self.hist_sort is not None:
+            self.history.sort(key=self.hist_sort)
 
-            if self.hist_sort is not None:
-                self.history.sort(key=self.hist_sort)
+        self.fv.make_async_gui_callback('add-image-info', self, info)
+
+        # image was newly added
+        return True
 
     def add_history(self, imname, path, idx=None,
                     image_loader=None, image_future=None):
@@ -277,18 +313,28 @@ class Channel(Callback.Callbacks):
                                time_added=time.time(),
                                time_modified=None)
             self._add_info(info)
+
         else:
             # already in history
             info = self.image_index[imname]
+
         return info
 
     def remove_history(self, imname):
         if imname in self.image_index:
             info = self.image_index[imname]
             del self.image_index[imname]
+            i = self.history.index(info)
             self.history.remove(info)
-            return info
-        return None
+
+            # adjust cursor as necessary
+            if i < self.cursor:
+                self.cursor -= 1
+            if self.cursor >= len(self.history):
+                # loop
+                self.cursor = min(0, len(self.history) - 1)
+
+            self.fv.make_async_gui_callback('remove-image-info', self, info)
 
     def get_current_image(self):
         return self.viewer.get_image()
@@ -320,40 +366,39 @@ class Channel(Callback.Callbacks):
 
     def switch_image(self, image):
 
-        with self.lock:
-            curimage = self.get_current_image()
-            if curimage != image:
-                self.logger.debug("updating viewer...")
-                self.view_object(image)
-
-                # update cursor to match image
-                imname = image.get('name')
-                if imname in self.image_index:
-                    info = self.image_index[imname]
-                    if info in self.history:
-                        self.cursor = self.history.index(info)
-
-                self.fv.channel_image_updated(self, image)
-
-                # Check for preloading any images into memory
-                preload = self.settings.get('preload_images', False)
-                if not preload:
-                    return
-
-                # queue next and previous files for preloading
-                index = self.cursor
-                if index < len(self.history)-1:
-                    info = self.history[index+1]
-                    if info.path is not None:
-                        self.fv.add_preload(self.name, info)
-
-                if index > 0:
-                    info = self.history[index-1]
-                    if info.path is not None:
-                        self.fv.add_preload(self.name, info)
-
-            else:
-                self.logger.debug("Apparently no need to set image.")
+        curimage = self.get_current_image()
+        if curimage != image:
+            self.logger.debug("updating viewer...")
+            self.view_object(image)
+
+            # update cursor to match image
+            imname = image.get('name')
+            if imname in self.image_index:
+                info = self.image_index[imname]
+                if info in self.history:
+                    self.cursor = self.history.index(info)
+
+            self.fv.channel_image_updated(self, image)
+
+            # Check for preloading any images into memory
+            preload = self.settings.get('preload_images', False)
+            if not preload:
+                return
+
+            # queue next and previous files for preloading
+            index = self.cursor
+            if index < len(self.history)-1:
+                info = self.history[index+1]
+                if info.path is not None:
+                    self.fv.add_preload(self.name, info)
+
+            if index > 0:
+                info = self.history[index-1]
+                if info.path is not None:
+                    self.fv.add_preload(self.name, info)
+
+        else:
+            self.logger.debug("Apparently no need to set channel viewer.")
 
     def switch_name(self, imname):
 
@@ -428,4 +473,10 @@ class Channel(Callback.Callbacks):
     def __len__(self):
         return len(self.history)
 
+    def __contains__(self, imname):
+        return imname in self.image_index
+
+    def __getitem__(self, imname):
+        return self.image_index[imname]
+
 # END
diff --git a/ginga/rv/Control.py b/ginga/rv/Control.py
index 3c6268c3..5e466ebc 100644
--- a/ginga/rv/Control.py
+++ b/ginga/rv/Control.py
@@ -111,7 +111,7 @@ class GingaShell(GwMain.GwMain, Widgets.Application):
         # For callbacks
         for name in ('add-image', 'channel-change', 'remove-image',
                      'add-channel', 'delete-channel', 'field-info',
-                     'add-image-info', 'add-operation'):
+                     'add-image-info', 'remove-image-info', 'add-operation'):
             self.enable_callback(name)
 
         # Initialize the timer factory
@@ -892,13 +892,23 @@ class GingaShell(GwMain.GwMain, Widgets.Application):
                 str(imname), str(e)))
 
     def redo_plugins(self, image, channel):
+        if image is not None:
+            imname = image.get('name', None)
+            if (imname is not None) and (imname not in channel):
+                # image may have been removed--
+                # skip updates to this channel's plugins
+                return
+
         # New data in channel
         # update active global plugins
         opmon = self.gpmon
         for key in opmon.get_active():
             obj = opmon.get_plugin(key)
             try:
-                self.gui_do(obj.redo, channel, image)
+                if image is None:
+                    self.gui_do(obj.blank, channel)
+                else:
+                    self.gui_do(obj.redo, channel, image)
 
             except Exception as e:
                 self.logger.error(
@@ -910,7 +920,10 @@ class GingaShell(GwMain.GwMain, Widgets.Application):
         for key in opmon.get_active():
             obj = opmon.get_plugin(key)
             try:
-                self.gui_do(obj.redo)
+                if image is None:
+                    self.gui_do(obj.blank)
+                else:
+                    self.gui_do(obj.redo)
 
             except Exception as e:
                 self.logger.error(
@@ -938,13 +951,14 @@ class GingaShell(GwMain.GwMain, Widgets.Application):
 
             # add cb so that if image is modified internally
             #  our plugins get updated
-            image.add_callback('modified', self.redo_plugins, channel)
+            if image is not None:
+                image.add_callback('modified', self.redo_plugins, channel)
 
             self.logger.debug("executing redo() in plugins...")
             self.redo_plugins(image, channel)
 
             split_time1 = time.time()
-            self.logger.info("Large image update: %.4f sec" % (
+            self.logger.info("Channel image update: %.4f sec" % (
                 split_time1 - start_time))
 
     def change_channel(self, chname, image=None, raisew=True):
@@ -1233,8 +1247,6 @@ class GingaShell(GwMain.GwMain, Widgets.Application):
                 viewer.clear()
 
         channel.remove_image(imname)
-        self.make_async_gui_callback('remove-image',
-                                     channel.name, imname, impath)
 
     def move_image_by_name(self, from_chname, imname, to_chname, impath=None):
 
diff --git a/ginga/rv/plugins/Catalogs.py b/ginga/rv/plugins/Catalogs.py
index aee7a607..d2abbb4d 100644
--- a/ginga/rv/plugins/Catalogs.py
+++ b/ginga/rv/plugins/Catalogs.py
@@ -317,7 +317,10 @@ class Catalogs(GingaPlugin.LocalPlugin):
 
     def stop(self):
         # stop catalog operation
-        self.clear_all()
+        try:
+            self.clear_all()
+        except:
+            pass
         # remove the canvas from the image
         self.canvas.ui_set_active(False)
         p_canvas = self.fitsimage.get_canvas()
diff --git a/ginga/rv/plugins/ChangeHistory.py b/ginga/rv/plugins/ChangeHistory.py
index 42554d4c..3f6cf23a 100644
--- a/ginga/rv/plugins/ChangeHistory.py
+++ b/ginga/rv/plugins/ChangeHistory.py
@@ -73,7 +73,8 @@ class ChangeHistory(GingaPlugin.GlobalPlugin):
                                    ts_colwidth=250)
         self.settings.load(onError='silent')
 
-        fv.add_callback('remove-image', self.remove_image_cb)
+        fv.add_callback('add-image-info', self.add_image_info_cb)
+        fv.add_callback('remove-image-info', self.remove_image_info_cb)
         fv.add_callback('delete-channel', self.delete_channel_cb)
 
         self.gui_up = False
@@ -187,25 +188,35 @@ class ChangeHistory(GingaPlugin.GlobalPlugin):
     def redo(self, channel, image):
         """Add an entry with image modification info."""
         chname = channel.name
+        if image is None:
+            # shouldn't happen, but let's play it safe
+            return
+
         imname = image.get('name', 'none')
         iminfo = channel.get_image_info(imname)
         timestamp = iminfo.time_modified
 
-        # Image fell out of cache and lost its history
         if timestamp is None:
-            self.remove_image_cb(self.fv, chname, imname, image.get('path'))
+            # Image somehow lost its history
+            self.remove_image_info_cb(self.fv, channel, iminfo)
             return
 
+        self.add_entry(chname, iminfo)
+
+    def add_entry(self, chname, iminfo):
+
         # Add info to internal log
         if chname not in self.name_dict:
             self.name_dict[chname] = {}
 
         fileDict = self.name_dict[chname]
 
+        imname = iminfo.name
         if imname not in fileDict:
             fileDict[imname] = {}
 
         # Z: Zulu time, GMT, UTC
+        timestamp = iminfo.time_modified
         timestamp = timestamp.strftime('%Y-%m-%dZ %H:%M:%SZ')
         reason = iminfo.get('reason_modified', 'Not given')
         bnch = Bunch.Bunch(CHNAME=chname, NAME=imname, MODIFIED=timestamp,
@@ -221,13 +232,15 @@ class ChangeHistory(GingaPlugin.GlobalPlugin):
         if self.gui_up:
             self.recreate_toc()
 
-    def remove_image_cb(self, viewer, chname, name, path):
+    def remove_image_info_cb(self, gshell, channel, iminfo):
         """Delete entries related to deleted image."""
+        chname = channel.name
         if chname not in self.name_dict:
             return
 
         fileDict = self.name_dict[chname]
 
+        name = iminfo.name
         if name not in fileDict:
             return
 
@@ -240,7 +253,17 @@ class ChangeHistory(GingaPlugin.GlobalPlugin):
         self.clear_selected_history()
         self.recreate_toc()
 
-    def delete_channel_cb(self, viewer, chinfo):
+    def add_image_info_cb(self, gshell, channel, iminfo):
+        """Add entries related to an added image."""
+
+        timestamp = iminfo.time_modified
+        if timestamp is None:
+            # Not an image we are interested in tracking
+            return
+
+        self.add_entry(channel.name, iminfo)
+
+    def delete_channel_cb(self, gshell, chinfo):
         """Called when a channel is deleted from the main interface.
         Parameter is chinfo (a bunch)."""
         chname = chinfo.name
diff --git a/ginga/rv/plugins/Contents.py b/ginga/rv/plugins/Contents.py
index abb266c8..69e5d9c3 100644
--- a/ginga/rv/plugins/Contents.py
+++ b/ginga/rv/plugins/Contents.py
@@ -5,7 +5,7 @@
 # Please see the file LICENSE.txt for details.
 #
 from ginga.util.six import itervalues
-from ginga.util.six.moves import map
+from ginga.util.six.moves import map, filter
 
 from ginga import GingaPlugin
 from ginga.misc import Bunch
@@ -62,10 +62,12 @@ class Contents(GingaPlugin.GlobalPlugin):
         self.highlight_tracks_keyboard_focus = self.settings.get(
             'highlight_tracks_keyboard_focus', True)
         self._hl_path = set([])
+        self.chnames = []
 
         fv.add_callback('add-image', self.add_image_cb)
-        fv.add_callback('add-image-info', self.add_image_info_cb)
         fv.add_callback('remove-image', self.remove_image_cb)
+        fv.add_callback('add-image-info', self.add_image_info_cb)
+        fv.add_callback('remove-image-info', self.remove_image_info_cb)
         fv.add_callback('add-channel', self.add_channel_cb)
         fv.add_callback('delete-channel', self.delete_channel_cb)
         fv.add_callback('channel-change', self.focus_cb)
@@ -73,37 +75,103 @@ class Contents(GingaPlugin.GlobalPlugin):
         self.gui_up = False
 
     def build_gui(self, container):
+        vbox = Widgets.VBox()
+        vbox.set_border_width(2)
+        vbox.set_spacing(4)
+
         # create the Treeview
         always_expand = self.settings.get('always_expand', False)
         color_alternate = self.settings.get('color_alternate_rows', True)
         treeview = Widgets.TreeView(auto_expand=always_expand,
                                     sortable=True,
+                                    selection='multiple',
                                     use_alt_row_color=color_alternate)
         self.treeview = treeview
         treeview.setup_table(self.columns, 2, 'NAME')
 
-        treeview.add_callback('selected', self.switch_image)
-        container.add_widget(treeview, stretch=1)
-
+        treeview.add_callback('activated', self.dblclick_cb)
+        treeview.add_callback('selected', self.select_cb)
+        vbox.add_widget(treeview, stretch=1)
+
+        btns = Widgets.HBox()
+        btns.set_spacing(4)
+        b1 = Widgets.Button('Display')
+        b1.add_callback('activated', self.display_cb)
+        b1.set_tooltip("Display the selected object in its channel viewer")
+        b1.set_enabled(False)
+        btns.add_widget(b1)
+        b2 = Widgets.Button('Move')
+        b2.add_callback('activated', lambda w: self.ask_action_images('move'))
+        b2.set_tooltip("Move the selected objects to a channel")
+        b2.set_enabled(False)
+        btns.add_widget(b2)
+        b3 = Widgets.Button('Copy')
+        b3.add_callback('activated', lambda w: self.ask_action_images('copy'))
+        b3.set_tooltip("Copy the selected objects to a channel")
+        b3.set_enabled(False)
+        btns.add_widget(b3)
+        b4 = Widgets.Button('Remove')
+        b4.add_callback('activated', lambda w: self.ask_action_images('remove'))
+        b4.set_tooltip("Remove the selected objects from a channel")
+        b4.set_enabled(False)
+        btns.add_widget(b4)
+        btns.add_widget(Widgets.Label(''), stretch=1)
+        self.btn_list = [b1, b2, b3, b4]
+
+        self._rebuild_channels()
+
+        vbox.add_widget(btns, stretch=0)
+
+        container.add_widget(vbox, stretch=1)
         self.gui_up = True
 
     def stop(self):
         self.gui_up = False
 
-    def switch_image(self, widget, res_dict):
+    def get_selected(self):
+        res = []
+        res_dict = self.treeview.get_selected()
         if len(res_dict) == 0:
+            return res
+        for chname in res_dict.keys():
+            img_dict = res_dict[chname]
+            if len(img_dict) == 0:
+                continue
+            for imname in img_dict.keys():
+                bnch = img_dict[imname]
+                res.append((chname, bnch))
+        return res
+
+    def dblclick_cb(self, widget, d):
+        chname = list(d.keys())[0]
+        imname = list(d[chname].keys())[0]
+        bnch = d[chname][imname]
+        path = bnch.path
+        self.logger.debug("chname=%s name=%s path=%s" % (
+            chname, imname, path))
+
+        self.fv.switch_name(chname, imname, path=path,
+                            image_future=bnch.image_future)
+
+    def select_cb(self, widget, d):
+        res = self.get_selected()
+        tf = (len(res) > 0)
+        for btn in self.btn_list:
+            btn.set_enabled(tf)
+
+    def display_cb(self, widget):
+        res = self.get_selected()
+        if len(res) != 1:
+            self.fv.show_error("Please select just one file to display!")
             return
-        chname = list(res_dict.keys())[0]
-        img_dict = res_dict[chname]
-        if len(img_dict) == 0:
-            return
-        imname = list(img_dict.keys())[0]
-        bnch = img_dict[imname]
+
+        chname, bnch = res[0]
+
         if not 'path' in bnch:
             # may be a top-level channel node, e.g. in gtk
             return
-
         path = bnch.path
+        imname = bnch.imname
         self.logger.debug("chname=%s name=%s path=%s" % (
             chname, imname, path))
 
@@ -208,7 +276,7 @@ class Contents(GingaPlugin.GlobalPlugin):
         self.logger.debug("%s added to Contents" % (name))
 
     def add_image_info_cb(self, viewer, channel, image_info):
-        """Almost the same as add_image_info(), except that the image
+        """Almost the same as add_image_cb(), except that the image
         may not be loaded in memory.
         """
         chname = channel.name
@@ -247,6 +315,12 @@ class Contents(GingaPlugin.GlobalPlugin):
         self.recreate_toc()
         self.logger.debug("%s removed from Contents" % (name))
 
+    def remove_image_info_cb(self, viewer, channel, image_info):
+        """Almost the same as remove_image_cb().
+        """
+        return self.remove_image_cb(viewer, channel.name,
+                                    image_info.name, image_info.path)
+
     def clear(self):
         self.name_dict = Bunch.caselessDict()
         self._hl_path = set([])
@@ -270,6 +344,8 @@ class Contents(GingaPlugin.GlobalPlugin):
         tree_dict = { chname: { } }
         self.treeview.add_tree(tree_dict)
 
+        self._rebuild_channels()
+
     def delete_channel_cb(self, viewer, channel):
         """Called when a channel is deleted from the main interface.
         Parameter is a channel (a Channel object)."""
@@ -287,6 +363,11 @@ class Contents(GingaPlugin.GlobalPlugin):
             return False
         self.recreate_toc()
 
+        self._rebuild_channels()
+
+    def _rebuild_channels(self):
+        self.chnames = self.fv.get_channel_names()
+
     def _get_hl_key(self, chname, image):
         return (chname, image.get('name', 'none'))
 
@@ -304,7 +385,7 @@ class Contents(GingaPlugin.GlobalPlugin):
         try:
             self.treeview.highlight_path(hl_path, tf, font_color=fc)
         except Exception as e:
-            self.logger.error('Error changing highlight on treeview path '
+            self.logger.info('Error changing highlight on treeview path '
                               '({0}): {1}'.format(hl_path, str(e)))
 
     def update_highlights(self, old_highlight_set, new_highlight_set):
@@ -374,6 +455,74 @@ class Contents(GingaPlugin.GlobalPlugin):
             self.update_highlights(self._hl_path, new_highlight)
             self._hl_path = new_highlight
 
+    def ask_action_images(self, action):
+
+        images = self.get_selected()
+        if len(images) == 0:
+            self.fv.show_error("Please select some images first")
+            return
+
+        l_img = list(map(lambda tup: "%s/%s" % (tup[0], tup[1].imname),
+                         images))
+
+        verb = action.capitalize()
+        l_img.insert(0, "%s images\n" % (verb))
+
+        # build dialog
+        dialog = Widgets.Dialog(title="%s Images" % (verb),
+                                flags=0,
+                                buttons=[['Cancel', 0], ['Ok', 1]],
+                                parent=self.treeview)
+        box = dialog.get_content_area()
+        box.set_border_width(6)
+        if len(l_img) < 12:
+            text = Widgets.Label("\n".join(l_img))
+        else:
+            text = Widgets.TextArea(wrap=None)
+            text.set_text("\n".join(l_img))
+        box.add_widget(text, stretch=1)
+
+        if action != 'remove':
+            hbox = Widgets.HBox()
+            hbox.add_widget(Widgets.Label("To channel: "))
+            chnl = Widgets.ComboBox()
+            for chname in self.chnames:
+                chnl.append_text(chname)
+            hbox.add_widget(chnl)
+            hbox.add_widget(Widgets.Label(''), stretch=1)
+            box.add_widget(hbox)
+        else:
+            chnl = None
+
+        dialog.add_callback('activated',
+                            lambda w, rsp: self.action_images_cb(w, rsp,
+                                                                 chnl,
+                                                                 images,
+                                                                 action))
+
+        self.fv.ds.show_dialog(dialog)
+
+    def action_images_cb(self, w, rsp, chnl_w, images, action):
+        # dst channel
+        if chnl_w is not None:
+            idx = chnl_w.get_index()
+            chname = self.chnames[idx]
+            dst_channel = self.fv.get_channel(chname)
+
+        self.fv.ds.remove_dialog(w)
+        if rsp == 0:
+            # user canceled
+            return
+
+        for chname, info in images:
+            src_channel = self.fv.get_channel(chname)
+            if action == 'copy':
+                src_channel.copy_image_to(info.imname, dst_channel)
+            elif action == 'move':
+                src_channel.move_image_to(info.imname, dst_channel)
+            elif action == 'remove':
+                src_channel.remove_image(info.imname)
+
     def __str__(self):
         return 'contents'
 
diff --git a/ginga/rv/plugins/Header.py b/ginga/rv/plugins/Header.py
index a3b52897..3f4a405a 100644
--- a/ginga/rv/plugins/Header.py
+++ b/ginga/rv/plugins/Header.py
@@ -154,13 +154,18 @@ class Header(GingaPlugin.GlobalPlugin):
             self.add_channel(self.fv, channel)
 
     def redo(self, channel, image):
-        """This is called when buffer is modified."""
+        """This is called when image changes."""
         self._image = None  # Skip cache checking in set_header()
-        chname = channel.name
         info = channel.extdata._header_info
 
         self.set_header(info, image)
 
+    def blank(self, channel):
+        """This is called when image is cleared."""
+        self._image = None
+        info = channel.extdata._header_info
+        info.table.clear()
+
     def set_sortable_cb(self, info):
         self._image = None
         channel = self.fv.get_channel(info.chname)
diff --git a/ginga/rv/plugins/Mosaic.py b/ginga/rv/plugins/Mosaic.py
index 6ad31e3d..31ca4297 100644
--- a/ginga/rv/plugins/Mosaic.py
+++ b/ginga/rv/plugins/Mosaic.py
@@ -84,6 +84,8 @@ class Mosaic(GingaPlugin.LocalPlugin):
         # hook to allow special processing before inlining
         self.preprocess = lambda x: x
 
+        self.fv.add_callback('remove-image', self._remove_image_cb)
+
         self.gui_up = False
 
 
@@ -321,9 +323,13 @@ class Mosaic(GingaPlugin.LocalPlugin):
         time_intr1 = time.time()
 
         # Add description for ChangeHistory
-        iminfo = self.channel.get_image_info(self.img_mosaic.get('name'))
-        iminfo.reason_modified = 'Added {0}'.format(
-            ','.join([im.get('name') for im in images]))
+        try:
+            iminfo = self.channel.get_image_info(self.img_mosaic.get('name'))
+            iminfo.reason_modified = 'Added {0}'.format(
+                ','.join([im.get('name') for im in images]))
+        except KeyError:
+            # mosaic image may have been deleted from channel
+            pass
 
         loc = self.img_mosaic.mosaic_inline(images,
                                             bg_ref=bg_ref,
@@ -583,6 +589,13 @@ class Mosaic(GingaPlugin.LocalPlugin):
         self.w.num_threads.set_text(str(num_threads))
         self.settings.set(num_threads=num_threads)
 
+    def _remove_image_cb(self, fv, chname, imname, impath):
+        # clear our handle to the mosaic image if it has been
+        # deleted from the channel
+        if self.img_mosaic is not None:
+            if imname == self.img_mosaic.get('name', None):
+                self.img_mosaic = None
+
     def update_status(self, text):
         if self.gui_up:
             self.fv.gui_do(self.w.eval_status.set_text, text)
diff --git a/ginga/rv/plugins/Pan.py b/ginga/rv/plugins/Pan.py
index 7d9d21a7..38b0d85f 100644
--- a/ginga/rv/plugins/Pan.py
+++ b/ginga/rv/plugins/Pan.py
@@ -181,8 +181,7 @@ class Pan(GingaPlugin.GlobalPlugin):
         paninfo = channel.extdata._pan_info
 
         if (image is None) or not isinstance(image, BaseImage):
-            # TODO: clear not yet working
-            #paninfo.panimage.clear()
+            paninfo.panimage.clear()
             return
 
         loval, hival = channel.fitsimage.get_cut_levels()
@@ -190,6 +189,10 @@ class Pan(GingaPlugin.GlobalPlugin):
 
         self.set_image(channel, paninfo, image)
 
+    def blank(self, channel):
+        paninfo = channel.extdata._pan_info
+        paninfo.panimage.clear()
+
     def focus_cb(self, viewer, channel):
         chname = channel.name
 
@@ -237,8 +240,7 @@ class Pan(GingaPlugin.GlobalPlugin):
 
     def set_image(self, channel, paninfo, image):
         if (image is None) or not isinstance(image, BaseImage):
-            # TODO: clear not yet working
-            #paninfo.panimage.clear()
+            paninfo.panimage.clear()
             return
 
         if not self.use_shared_canvas:
@@ -285,8 +287,7 @@ class Pan(GingaPlugin.GlobalPlugin):
     def panset(self, fitsimage, channel, paninfo):
         image = fitsimage.get_image()
         if (image is None) or not isinstance(image, BaseImage):
-            # TODO: clear not yet working
-            #paninfo.panimage.clear()
+            paninfo.panimage.clear()
             return
 
         x, y = fitsimage.get_pan()
diff --git a/ginga/rv/plugins/Thumbs.py b/ginga/rv/plugins/Thumbs.py
index b22ef247..1b564d32 100644
--- a/ginga/rv/plugins/Thumbs.py
+++ b/ginga/rv/plugins/Thumbs.py
@@ -5,6 +5,7 @@
 # Please see the file LICENSE.txt for details.
 #
 import os
+import time
 import threading
 
 from ginga import GingaPlugin
@@ -13,6 +14,8 @@ from ginga.misc import Bunch
 from ginga.util import iohelper
 from ginga.gw import Widgets, Viewers
 from ginga.util.paths import icondir
+from ginga import cmap, RGBImage
+from ginga.pilw.ImageViewPil import CanvasView
 
 class Thumbs(GingaPlugin.GlobalPlugin):
     """
@@ -33,8 +36,7 @@ class Thumbs(GingaPlugin.GlobalPlugin):
     Clicking on a thumbnail navigates you directly to that image in the
     associated channel.  Hovering the cursor over a thumbnail will show a
     tool tip that contains a couple of useful pieces of metadata from the
-    image.  Right-clicking on a thumbnail brings up a context menu with
-    options for displaying or removing an image.
+    image.
 
     The "Auto Scroll" checkbox, if checked, will cause the Thumbs pan to
     scroll to the active image.
@@ -49,8 +51,11 @@ class Thumbs(GingaPlugin.GlobalPlugin):
         self.thumb_num_cols = 1
         self.thumb_row_count = 0
         self.thumb_col_count = 0
-        # distance in pixels between thumbs
-        self.thumb_sep = 15
+        self._wd = 300
+        self._ht = 400
+        self._cmxoff = 10
+        self._cmyoff = 10
+        self._max_y = 0
         tt_keywords = ['OBJECT', 'FRAMEID', 'UT', 'DATE-OBS']
 
         prefs = self.fv.get_preferences()
@@ -58,25 +63,45 @@ class Thumbs(GingaPlugin.GlobalPlugin):
         self.settings.add_defaults(cache_thumbs=False,
                                    cache_location='local',
                                    auto_scroll=True,
-                                   rebuild_wait=4.0,
+                                   rebuild_wait=0.5,
                                    tt_keywords=tt_keywords,
                                    mouseover_name_key='NAME',
-                                   thumb_length=192,
+                                   thumb_length=180,
+                                   thumb_hsep=15,
+                                   thumb_vsep=15,
                                    sort_order=None,
                                    label_length=25,
                                    label_cutoff='right',
                                    highlight_tracks_keyboard_focus=True,
-                                   label_font_color='black',
+                                   label_font_color='white',
+                                   label_font_size=10,
                                    label_bg_color='lightgreen')
         self.settings.load(onError='silent')
         # max length of thumb on the long side
-        self.thumb_width = self.settings.get('thumb_length', 150)
+        self.thumb_width = self.settings.get('thumb_length', 180)
+        # distance in pixels between thumbs
+        self.thumb_hsep = self.settings.get('thumb_hsep', 15)
+        self.thumb_vsep = self.settings.get('thumb_vsep', 15)
+
+        # Build our thumb generator
+        tg = CanvasView(logger=self.logger)
+        tg.configure_surface(self.thumb_width, self.thumb_width)
+        tg.enable_autozoom('on')
+        tg.set_autocut_params('zscale')
+        tg.enable_autocuts('override')
+        tg.enable_auto_orient(True)
+        tg.defer_redraw = False
+        tg.set_bg(0.7, 0.7, 0.7)
+        self.thumb_generator = tg
 
         self.thmbtask = fv.get_timer()
         self.thmbtask.set_callback('expired', self.redo_delay_timer)
-        self.lagtime = self.settings.get('rebuild_wait', 4.0)
+        self.lagtime = self.settings.get('rebuild_wait', 0.5)
         self.thmblock = threading.RLock()
 
+        # this will hold the thumbnails pane viewer
+        self.c_view = None
+
         # TODO: these maybe should be configurable by channel
         # different instruments have different keywords of interest
         self.keywords = self.settings.get('tt_keywords', tt_keywords)
@@ -88,7 +113,7 @@ class Thumbs(GingaPlugin.GlobalPlugin):
 
         fv.set_callback('add-image', self.add_image_cb)
         fv.set_callback('add-image-info', self.add_image_info_cb)
-        fv.set_callback('remove-image', self.remove_image_cb)
+        fv.set_callback('remove-image-info', self.remove_image_info_cb)
         fv.set_callback('add-channel', self.add_channel_cb)
         fv.set_callback('delete-channel', self.delete_channel_cb)
         fv.add_callback('channel-change', self.focus_cb)
@@ -96,34 +121,44 @@ class Thumbs(GingaPlugin.GlobalPlugin):
         self.gui_up = False
 
     def build_gui(self, container):
-        # width, height = 300, 300
-        # cm, im = self.fv.cm, self.fv.im
-
-        thumb_len = self.settings.get('thumb_length', 192)
-
-        tg = Viewers.ImageViewCanvas(logger=self.logger)
-        tg.configure_window(thumb_len, thumb_len)
-        tg.enable_autozoom('on')
-        tg.set_autocut_params('zscale')
-        tg.enable_autocuts('override')
-        tg.enable_auto_orient(True)
-        tg.defer_redraw = False
-        tg.set_bg(0.7, 0.7, 0.7)
-        self.thumb_generator = tg
-
-        sw = Widgets.ScrollArea()
-        sw.add_callback('configure', self.thumbpane_resized_cb)
-
-        # Create thumbnails pane
-        vbox = Widgets.GridBox()
-        vbox.set_margins(4, 4, 4, 4)
-        vbox.set_column_spacing(14)
-        self.w.thumbs = vbox
-
-        sw.set_widget(vbox)
-        self.w.thumbs_scroll = sw
-
-        container.add_widget(sw, stretch=1)
+        vbox = Widgets.VBox()
+        vbox.set_border_width(4)
+        vbox.set_spacing(2)
+
+        # construct an interaactive viewer to view and scroll
+        # the RGB image, and to let the user pick the cmap
+        self.c_view = Viewers.CanvasView(logger=self.logger)
+        c_v = self.c_view
+        c_v.set_desired_size(self._wd, self._ht)
+        c_v.enable_autozoom('off')
+        c_v.enable_autocuts('off')
+        c_v.set_pan(0, 0)
+        c_v.scale_to(1.0, 1.0)
+        c_v.transform(False, True, False)
+        c_v.cut_levels(0, 255)
+        c_v.set_bg(0.4, 0.4, 0.4)
+        # for debugging
+        c_v.set_name('cmimage')
+        c_v.add_callback('configure', self.thumbpane_resized_cb)
+        c_v.add_callback('drag-drop', self.drag_drop_cb)
+
+        canvas = c_v.get_canvas()
+        canvas.register_for_cursor_drawing(c_v)
+        c_v.add_callback('scroll', self.scroll_cb)
+        canvas.set_draw_mode('pick')
+        canvas.ui_set_active(True)
+        self.canvas = canvas
+
+        bd = c_v.get_bindings()
+        bd.enable_pan(True)
+        # disable zooming so scrolling can be used to pan up/down
+        bd.enable_zoom(False)
+        bd.enable_cmap(False)
+
+        iw = Viewers.GingaScrolledViewerWidget(c_v)
+        iw.resize(self._wd, self._ht)
+
+        vbox.add_widget(iw, stretch=1)
 
         captions = (('Auto scroll', 'checkbutton', 'Clear', 'button'),)
         w, b = Widgets.build_info(captions)
@@ -135,10 +170,37 @@ class Thumbs(GingaPlugin.GlobalPlugin):
         b.clear.add_callback('activated', lambda w: self.clear())
         auto_scroll = self.settings.get('auto_scroll', True)
         b.auto_scroll.set_state(auto_scroll)
-        container.add_widget(w, stretch=0)
+        vbox.add_widget(w, stretch=0)
+
+        container.add_widget(vbox, stretch=1)
 
         self.gui_up = True
 
+    def scroll_cb(self, viewer, direction, amt, data_x, data_y):
+        """Called when the user scrolls in the thumb pane.
+        Pan up or down to show additional thumbs.
+        """
+        bd = viewer.get_bindings()
+        direction = bd.get_direction(direction)
+        pan_x, pan_y = viewer.get_pan()[:2]
+        qty = self.thumb_vsep * amt * self.settings.get('thumb_pan_accel', 1.0)
+        if direction == 'up':
+            pan_y -= qty
+        else:
+            pan_y += qty
+
+        limits = viewer.get_limits(coord='data')
+        pan_y = min(max(pan_y, limits[0][1]), limits[1][1])
+
+        viewer.set_pan(pan_x, pan_y)
+
+    def drag_drop_cb(self, viewer, urls):
+        """Punt drag-drops to the ginga shell.
+        """
+        channel = self.fv.get_current_channel()
+        self.fv.dragdrop(channel.viewer, urls)
+        return True
+
     def _get_thumb_key(self, chname, image):
         path = image.get('path', None)
         return self.get_thumb_key(chname, image.get('name'), path)
@@ -149,68 +211,52 @@ class Thumbs(GingaPlugin.GlobalPlugin):
         thumbkey = (chname, imname, path)
         return thumbkey
 
-    def add_image_cb(self, viewer, chname, image, image_info):
-        if not self.gui_up:
-            return False
+    def add_image_cb(self, viewer, chname, image, info):
 
-        # image is flagged not to make a thumbnail?
-        nothumb = image.get('nothumb', False)
-        if nothumb:
-            return
-
-        # idx = image.get('idx', None)
-        # get image path
-        path = image_info.path
+        # Get any previously stored thumb information in the image info
+        thumb_extra = info.setdefault('thumb_extras', Bunch.Bunch())
 
-        if path is not None:
-            path = os.path.abspath(path)
-        name = image_info.name
+        # Get metadata for mouse-over tooltip
+        metadata = self._get_tooltip_metadata(info, image)
 
-        thumbname = name
-        self.logger.info("making thumb for %s" % (thumbname))
+        # Update the tooltip, in case of new or changed metadata
+        text = self._mk_tooltip_text(metadata)
+        thumb_extra.tooltip = text
 
-        future = image_info.image_future
+        if not self.gui_up:
+            return False
 
-        # Is there a preference set to avoid making thumbnails?
         channel = self.fv.get_channel(chname)
-        prefs = channel.settings
-        if not prefs.get('genthumb', False):
-            return
 
-        # Is this thumbnail already in the list?
-        # NOTE: does not handle two separate images with the same path
-        # in the same channel
-        thumbkey = self.get_thumb_key(chname, name, path)
-        with self.thmblock:
-            if thumbkey in self.thumb_dict:
-                self.logger.debug("thumb key already present")
-                return
+        if thumb_extra.get('time_update', None) is None:
+            self.fv.gui_do(self.redo_thumbnail_image, channel, image, info)
 
-        # Get metadata for mouse-over tooltip
-        header = image.get_header()
-        metadata = {}
-        for kwd in self.keywords:
-            metadata[kwd] = header.get(kwd, 'N/A')
-        metadata[self.settings.get('mouseover_name_key', 'NAME')] = name
+    def add_image_info_cb(self, viewer, channel, info):
 
-        thumbpath = self.get_thumbpath(path)
+        save_thumb = self.settings.get('cache_thumbs', False)
+
+        # Do we already have this thumb loaded?
+        chname = channel.name
+        thumbkey = self.get_thumb_key(chname, info.name, info.path)
+        thumbpath = self.get_thumbpath(info.path)
 
         with self.thmblock:
-            self.copy_attrs(channel.fitsimage)
-            thumb_np = image.get_thumbnail(self.thumb_width)
-            self.thumb_generator.set_data(thumb_np)
-            imgwin = self.thumb_generator.get_plain_image_as_widget()
+            try:
+                bnch = self.thumb_dict[thumbkey]
+                # if these are not equal then the mtime must have
+                # changed on the file, better reload and regenerate
+                if bnch.thumbpath == thumbpath:
+                    self.logger.debug("we have this thumb--skipping regeneration")
+                    return
+                self.logger.debug("we have this thumb, but thumbpath is different--regenerating thumb")
+            except KeyError:
+                self.logger.debug("we don't seem to have this thumb--generating thumb")
 
-        label_length = self.settings.get('label_length', None)
-        label_cutoff = self.settings.get('label_cutoff', 'right')
+        thmb_image = self._get_thumb_image(channel, info, None)
 
-        # Shorten thumbnail label, if requested
-        if label_length is not None:
-            thumbname = iohelper.shorten_name(thumbname, label_length,
-                                              side=label_cutoff)
+        self.fv.gui_do(self._make_thumb, chname, thmb_image, info, thumbkey,
+                       save_thumb=save_thumb, thumbpath=thumbpath)
 
-        self.insert_thumbnail(imgwin, thumbkey, thumbname, chname, name, path,
-                              thumbpath, metadata, future)
 
     def _add_image(self, viewer, chname, image):
         channel = self.fv.get_channel(chname)
@@ -224,27 +270,25 @@ class Thumbs(GingaPlugin.GlobalPlugin):
 
         self.add_image_cb(viewer, chname, image, info)
 
-    def remove_image_cb(self, viewer, chname, name, path):
+    def remove_image_info_cb(self, viewer, channel, image_info):
         if not self.gui_up:
             return
 
-        self.logger.info("removing thumb for %s" % (name))
-
+        chname, imname, impath = channel.name, image_info.name, image_info.path
         try:
-            # Is this thumbnail already in the list?
-            thumbkey = self.get_thumb_key(chname, name, path)
+            thumbkey = self.get_thumb_key(chname, imname, impath)
             self.remove_thumb(thumbkey)
         except Exception as e:
             self.logger.error("Error removing thumb for %s: %s" % (
-                name, str(e)))
+                imname, str(e)))
 
     def remove_thumb(self, thumbkey):
         with self.thmblock:
-            if thumbkey not in self.thumb_dict:
-                return
-            self.clear_widget()
-            del self.thumb_dict[thumbkey]
-            self.thumb_list.remove(thumbkey)
+            self.logger.debug("Removing thumb %s" % (str(thumbkey)))
+            if thumbkey in self.thumb_dict:
+                del self.thumb_dict[thumbkey]
+            if thumbkey in self.thumb_list:
+                self.thumb_list.remove(thumbkey)
 
             # Unhighlight
             chname = thumbkey[0]
@@ -252,12 +296,12 @@ class Thumbs(GingaPlugin.GlobalPlugin):
             self._tkf_highlight.discard(thumbkey)
             channel.extdata.thumbs_old_highlight.discard(thumbkey)
 
-        self.reorder_thumbs()
+        self.fv.gui_do_oneshot('thumbs-reorder', self.reorder_thumbs)
 
-    def update_thumbs(self, nameList):
+    def update_thumbs(self, name_list):
 
         # Remove old thumbs that are not in the dataset
-        invalid = set(self.thumb_list) - set(nameList)
+        invalid = set(self.thumb_list) - set(name_list)
         if len(invalid) > 0:
             with self.thmblock:
                 for thumbkey in invalid:
@@ -265,35 +309,39 @@ class Thumbs(GingaPlugin.GlobalPlugin):
                     del self.thumb_dict[thumbkey]
                     self._tkf_highlight.discard(thumbkey)
 
-            self.reorder_thumbs()
+        self.fv.gui_do_oneshot('thumbs-reorder', self.reorder_thumbs)
 
-    def thumbpane_resized_cb(self, widget, width, height):
-        self.logger.info("reordering thumbs width=%d" % (width))
+    def thumbpane_resized_cb(self, thumbvw, width, height):
+        self.fv.gui_do_oneshot('thumbs-resized', self._resized, width, height)
+
+    def _resized(self, width, height):
+        self.logger.debug("thumbs resized, width=%d" % (width))
 
         with self.thmblock:
-            cols = max(1, width // (self.thumb_width + self.thumb_sep))
+            cols = max(1, width // (self.thumb_width + self.thumb_hsep))
             if self.thumb_num_cols == cols:
                 # If we have not actually changed the possible number of columns
                 # then don't do anything
                 return False
-            self.logger.info("column count is now %d" % (cols))
+            self.logger.debug("column count is now %d" % (cols))
             self.thumb_num_cols = cols
 
-        self.reorder_thumbs()
+        self.fv.gui_do_oneshot('thumbs-reorder', self.reorder_thumbs)
         return False
 
-    def load_file(self, thumbkey, chname, name, path, image_future):
+    def load_file(self, thumbkey, chname, info):
         self.logger.debug("loading image: %s" % (str(thumbkey)))
-        self.fv.switch_name(chname, name, path=path,
-                            image_future=image_future)
+        # TODO: deal with channel object directly?
+        self.fv.switch_name(chname, info.name, path=info.path,
+                            image_future=info.image_future)
 
     def clear(self):
         with self.thmblock:
-            self.clear_widget()
             self.thumb_list = []
             self.thumb_dict = {}
             self._tkf_highlight = set([])
-        self.reorder_thumbs()
+
+        self.fv.gui_do_oneshot('thumbs-reorder', self.reorder_thumbs)
 
     def add_channel_cb(self, viewer, channel):
         """Called when a channel is added from the main interface.
@@ -320,15 +368,9 @@ class Thumbs(GingaPlugin.GlobalPlugin):
             thumbkey = self._get_thumb_key(chname, image)
             new_highlight = set([thumbkey])
 
-            # TODO: already calculated thumbkey, use simpler test
-            if not self.have_thumbnail(fitsimage, image):
-                # No memory of this thumbnail, so regenerate it
-                self._add_image(viewer, chname, image)
-                return
-
-            # Else schedule an update of the thumbnail for changes to
-            # cut levels, etc.
-            self.redo_delay(fitsimage)
+            if self.have_thumbnail(fitsimage, image):
+                # schedule an update of the thumbnail to pick up changes
+                self.redo_delay(fitsimage)
 
         else:
             # no image has the focus
@@ -360,12 +402,6 @@ class Thumbs(GingaPlugin.GlobalPlugin):
     def redo_delay_timer(self, timer):
         self.fv.gui_do(self.redo_thumbnail, timer.data.fitsimage)
 
-    def copy_attrs(self, fitsimage):
-        # Reflect transforms, colormap, etc.
-        fitsimage.copy_attributes(self.thumb_generator,
-                                  ['transforms', 'cutlevels',
-                                   'rgbmap'])
-
     def update_highlights(self, old_highlight_set, new_highlight_set):
         """Unhighlight the thumbnails represented by `old_highlight_set`
         and highlight the ones represented by new_highlight_set.
@@ -377,20 +413,22 @@ class Thumbs(GingaPlugin.GlobalPlugin):
             un_hilite_set = old_highlight_set - new_highlight_set
             re_hilite_set = new_highlight_set - old_highlight_set
 
+            # highlight new labels that should be
+            bg = self.settings.get('label_bg_color', 'lightgreen')
+            fg = self.settings.get('label_font_color', 'black')
+
             # unhighlight thumb labels that should NOT be highlighted any more
             for thumbkey in un_hilite_set:
                 if thumbkey in self.thumb_dict:
                     namelbl = self.thumb_dict[thumbkey].get('namelbl')
-                    namelbl.set_color(bg=None, fg=None)
-
-            # highlight new labels that should be
-            bg = self.settings.get('label_bg_color', 'lightgreen')
-            fg = self.settings.get('label_font_color', 'black')
+                    namelbl.color = fg
 
             for thumbkey in re_hilite_set:
                 if thumbkey in self.thumb_dict:
                     namelbl = self.thumb_dict[thumbkey].get('namelbl')
-                    namelbl.set_color(bg=bg, fg=fg)
+                    namelbl.color = bg
+
+        self.c_view.redraw(whence=3)
 
     def redo(self, channel, image):
         """This method is called when an image is set in a channel."""
@@ -448,112 +486,232 @@ class Thumbs(GingaPlugin.GlobalPlugin):
         with self.thmblock:
             return thumbkey in self.thumb_dict
 
-    def redo_thumbnail(self, fitsimage, save_thumb=None):
-        self.logger.debug("redoing thumbnail...")
+    def redo_thumbnail(self, viewer, save_thumb=None):
         # Get the thumbnail image
-        image = fitsimage.get_image()
+        image = viewer.get_image()
         if image is None:
             return
+
+        imname = image.get('name', None)
+        if imname is None:
+            return
+
+        chname = self.fv.get_channel_name(viewer)
+        channel = self.fv.get_channel(chname)
+        try:
+            info = channel[imname]
+        except KeyError:
+            # don't generate a thumbnail without info
+            return
+
+        self.redo_thumbnail_image(channel, image, info, save_thumb=save_thumb)
+
+
+    def redo_thumbnail_image(self, channel, image, info, save_thumb=None):
+        # image is flagged not to make a thumbnail?
+        nothumb = image.get('nothumb', False)
+        if nothumb:
+            return
+
+        self.logger.debug("redoing thumbnail ...")
         if save_thumb is None:
             save_thumb = self.settings.get('cache_thumbs', False)
 
-        chname = self.fv.get_channel_name(fitsimage)
+        # Get any previously stored thumb information in the image info
+        thumb_extra = info.setdefault('thumb_extras', Bunch.Bunch())
 
         # Get metadata for mouse-over tooltip
-        header = image.get_header()
-        metadata = {}
-        for kwd in self.keywords:
-            metadata[kwd] = header.get(kwd, 'N/A')
-
-        # Look up our version of the thumb
-        idx = image.get('idx', None)
-        path = image.get('path', None)
-        if path is not None:
-            path = os.path.abspath(path)
-            name = self.fv.name_image_from_path(path, idx=idx)
-        else:
-            name = 'NoName'
-
-        # get image name
-        name = image.get('name', name)
-        metadata[self.settings.get('mouseover_name_key', 'NAME')] = name
+        metadata = self._get_tooltip_metadata(info, image)
 
-        thumbkey = self.get_thumb_key(chname, name, path)
+        chname = channel.name
+        thumbkey = self.get_thumb_key(chname, info.name, info.path)
         with self.thmblock:
             if thumbkey not in self.thumb_dict:
                 # No memory of this thumbnail, so regenerate it
+                self.logger.debug("No memory of %s, adding..." % (str(thumbkey)))
                 self._add_image(self.fv, chname, image)
                 return
 
             # Generate new thumbnail
             self.logger.debug("generating new thumbnail")
-            fitsimage.copy_attributes(self.thumb_generator,
-                                      ['transforms', 'cutlevels',
-                                       'rgbmap'])
-
-            thumb_np = image.get_thumbnail(self.thumb_width)
-            self.thumb_generator.set_data(thumb_np)
+            thmb_image = self._regen_thumb_image(image, channel.fitsimage)
+            thumb_extra.time_update = time.time()
 
             # Save a thumbnail for future browsing
-            if save_thumb and path is not None:
-                thumbpath = self.get_thumbpath(path)
+            if save_thumb and info.path is not None:
+                thumbpath = self.get_thumbpath(info.path)
                 if thumbpath is not None:
                     if os.path.exists(thumbpath):
                         os.remove(thumbpath)
-                    self.thumb_generator.save_plain_image_as_file(thumbpath,
-                                                                  format='jpeg')
-
-            imgwin = self.thumb_generator.get_plain_image_as_widget()
+                    thmb_image.save_as_file(thumbpath)
 
-        self.update_thumbnail(thumbkey, imgwin, name, metadata)
+        self.update_thumbnail(thumbkey, thmb_image, metadata)
 
     def delete_channel_cb(self, viewer, channel):
         """Called when a channel is deleted from the main interface.
         Parameter is channel (a bunch)."""
         chname_del = channel.name
         # TODO: delete thumbs for this channel!
-        self.logger.info("deleting thumbs for channel '%s'" % (chname_del))
+        self.logger.debug("deleting thumbs for channel '%s'" % (chname_del))
         with self.thmblock:
-            self.clear_widget()
-            newThumbList = []
+            new_thumb_list = []
             un_hilite_set = set([])
             for thumbkey in self.thumb_list:
                 chname = thumbkey[0]
                 if chname != chname_del:
-                    newThumbList.append(thumbkey)
+                    new_thumb_list.append(thumbkey)
                 else:
                     del self.thumb_dict[thumbkey]
                     un_hilite_set.add(thumbkey)
-            self.thumb_list = newThumbList
+            self.thumb_list = new_thumb_list
             self._tkf_highlight -= un_hilite_set  # Unhighlight
 
-        self.reorder_thumbs()
+        self.fv.gui_do_oneshot('thumbs-reorder', self.reorder_thumbs)
 
-    def _make_thumb(self, chname, image, name, path, thumbkey,
-                    image_future, save_thumb=False, thumbpath=None):
-        # This is called by the make_thumbs() as a gui thread
-        with self.thmblock:
-            thumb_np = image.get_thumbnail(self.thumb_width)
-            self.thumb_generator.set_data(thumb_np)
+    def _get_tooltip_metadata(self, info, image, keywords=None):
+        # Get metadata for mouse-over tooltip
+        header = {}
+        if image is not None:
+            header = image.get_header()
+
+        if keywords is None:
+            keywords = self.keywords
+        metadata = {kwd: header.get(kwd, 'N/A')
+                    for kwd in keywords}
+
+        # assign a name in the metadata if we don't have one yet
+        name_key = self.settings.get('mouseover_name_key', 'NAME')
+        if metadata.setdefault(name_key, info.name) == 'N/A':
+            metadata[name_key] = info.name
+
+        return metadata
+
+    def make_tt(self, viewer, canvas, text, pt, obj, fontsize=10):
+        x1, y1, x2, y2 = obj.get_llur()
+
+        # Determine pop-up position on canvas.  Try to align a little below
+        # the thumbnail image and offset a bit
+        x = x1 + 10
+        y = y1 + 10
+        mxwd = 0
+        lines = text.split('\n')
+
+        point = canvas.dc.Point(x, y, radius=0, color='black', alpha=0.0)
+        rect = canvas.dc.Rectangle(x, y, x, y, color='black', fill=True,
+                                   fillcolor='lightyellow')
+        crdmap = viewer.get_coordmap('offset')
+        crdmap.refobj = point
+
+        l = [point, rect]
+        a, b = 2, 0
+        for line in lines:
+            text = canvas.dc.Text(a, b, text=line, color='black',
+                                  fontsize=fontsize)
+            text.crdmap = crdmap
+            l.append(text)
+            txt_wd, txt_ht = viewer.renderer.get_dimensions(text)
+            b += txt_ht + 2
+            text.y = b
+            mxwd = max(mxwd, txt_wd)
+        rect.x2 = rect.x1 + mxwd + 2
+        rect.y2 = rect.y1 + b + 4
+
+        obj = canvas.dc.CompoundObject(*l)
+        return obj
+
+    def show_tt(self, obj, canvas, event, pt,
+                thumbkey, chname, info, tf):
+
+        text = info.thumb_extras.tooltip
+
+        tag = '_$tooltip'
+        try:
+            canvas.delete_object_by_tag(tag)
+        except KeyError:
+            pass
+        if tf:
+            tt = self.make_tt(self.c_view, canvas, text, pt, obj)
+            canvas.add(tt, tag=tag)
+
+    def _regen_thumb_image(self, image, viewer):
+        self.logger.debug("generating new thumbnail")
+        if viewer is not None:
+            viewer.copy_attributes(self.thumb_generator,
+                                   ['transforms', 'cutlevels', 'rgbmap'])
+
+        thumb_np = image.get_thumbnail(self.thumb_width)
+        self.thumb_generator.set_data(thumb_np)
+
+        rgb_img = self.thumb_generator.get_image_as_array()
+        thmb_image = RGBImage.RGBImage(rgb_img)
+        return thmb_image
+
+    def _get_thumb_image(self, channel, info, image):
+
+        # Get any previously stored thumb information in the image info
+        thumb_extra = info.setdefault('thumb_extras', Bunch.Bunch())
+
+        # Choice [A]: is there a thumb image attached to the image info?
+        if 'rgbimg' in thumb_extra:
+            # yes
+            return thumb_extra.rgbimg
+
+        thumbpath = self.get_thumbpath(info.path)
+
+        # Choice [B]: is the full image available to make a thumbnail?
+        if image is None:
+            try:
+                image = channel.get_loaded_image(info.name)
+
+            except KeyError:
+                pass
+
+        if image is not None:
+            try:
+                thmb_image = self._regen_thumb_image(image, None)
+                thumb_extra.rgbimg = thmb_image
+                thumb_extra.time_update = time.time()
+                return thmb_image
+
+            except Exception as e:
+                self.logger.warning("Error generating thumbnail: %s" % (str(e)))
+
+        thmb_image = RGBImage.RGBImage()
+        thmb_image.set(name=info.name)
 
+        # Choice [C]: is there a cached thumbnail image on disk we can use?
+        if (thumbpath is not None) and os.path.exists(thumbpath):
+            try:
+                # try to load the thumbnail image
+                thmb_image.load_file(thumbpath)
+                thumb_extra.rgbimg = thmb_image
+                return thmb_image
+
+            except Exception as e:
+                self.logger.warning("Error loading thumbnail: %s" % (str(e)))
+
+        # Choice [D]: load a placeholder image
+        tmp_path = os.path.join(icondir, 'fits.png')
+        thmb_image.load_file(tmp_path)
+        thmb_image.set(path=None)
+
+        return thmb_image
+
+    def _make_thumb(self, chname, thmb_image, info, thumbkey,
+                    save_thumb=False, thumbpath=None):
+
+        # This is called by the plugin FBrowser.make_thumbs() as
+        # a gui thread
+        with self.thmblock:
             # Save a thumbnail for future browsing
             if save_thumb and (thumbpath is not None):
-                self.thumb_generator.save_plain_image_as_file(thumbpath,
-                                                              format='jpeg')
-
-            imgwin = self.thumb_generator.get_plain_image_as_widget()
+                thmb_image.save_as_file(thumbpath)
 
         # Get metadata for mouse-over tooltip
-        header = image.get_header()
-        metadata = {}
-        for kwd in self.keywords:
-            metadata[kwd] = header.get(kwd, 'N/A')
+        metadata = self._get_tooltip_metadata(info, None)
 
-        thumbname = name
-        self.insert_thumbnail(imgwin, thumbkey, thumbname,
-                              chname, name, path, thumbpath, metadata,
-                              image_future)
-        self.fv.update_pending(timeout=0.001)
+        self.insert_thumbnail(thmb_image, thumbkey, chname,
+                              thumbpath, metadata, info)
 
     def get_thumbpath(self, path, makedir=True):
         if path is None:
@@ -583,76 +741,132 @@ class Thumbs(GingaPlugin.GlobalPlugin):
 
         return thumbpath
 
-    def insert_thumbnail(self, imgwin, thumbkey, thumbname, chname, name, path,
-                         thumbpath, metadata, image_future):
+    def _calc_thumb_pos(self, row, col):
+        # TODO: should really be text ht
+        text_ht = self.thumb_vsep
 
+        # calc in window coords
+        twd_hplus = self.thumb_width + self.thumb_hsep
+        twd_vplus = self.thumb_width + text_ht + self.thumb_vsep
+        xt = self._cmxoff + (col * twd_hplus)
+        yt = self._cmyoff + (row * twd_vplus) + text_ht
+
+        # position of image
+        xi = xt
+        yi = yt + 6
+
+        # convert to data coords
+        crdmap = self.c_view.get_coordmap('window')
+        xtd, ytd = crdmap.to_data((xt, yt))
+        xid, yid = crdmap.to_data((xi, yi))
+
+        return (xtd, ytd, xid, yid)
+
+    def insert_thumbnail(self, imgwin, thumbkey, chname,
+                         thumbpath, metadata, info):
+
+        thumbname = info.name
         self.logger.debug("inserting thumb %s" % (thumbname))
         # make a context menu
-        menu = self._mk_context_menu(thumbkey, chname, name, path, image_future)
-
-        thumbw = Widgets.Image(native_image=imgwin, menu=menu,
-                               style='clickable')
-        thumbw.resize(self.thumb_width, self.thumb_width)
+        menu = self._mk_context_menu(thumbkey, chname, info)
 
-        # set the load callback
-        thumbw.add_callback('activated',
-                            lambda w: self.load_file(thumbkey, chname, name,
-                                                     path, image_future))
+        # Get any previously stored thumb information in the image info
+        thumb_extra = info.setdefault('thumb_extras', Bunch.Bunch())
 
-        # make a tool tip
-        text = self.query_thumb(thumbkey, name, metadata)
-        thumbw.set_tooltip(text)
+        # If there is no previously made tooltip, then generate one
+        if 'tooltip' in thumb_extra:
+            text = thumb_extra.tooltip
+        else:
+            text = self._mk_tooltip_text(metadata)
+            thumb_extra.tooltip = text
 
-        vbox = Widgets.VBox()
-        vbox.set_margins(0, 0, 0, 0)
-        vbox.set_spacing(0)
-        namelbl = Widgets.Label(text=thumbname, halign='left')
-        vbox.add_widget(namelbl, stretch=0)
-        vbox.add_widget(thumbw, stretch=0)
-        # special hack for Qt widgets
-        vbox.cfg_expand(0, 0)
-
-        bnch = Bunch.Bunch(widget=vbox, image=thumbw,
-                           name=name, imname=name, namelbl=namelbl,
-                           chname=chname, path=path, thumbpath=thumbpath,
-                           image_future=image_future)
+        canvas = self.c_view.get_canvas()
+        fg = self.settings.get('label_font_color', 'black')
+        fontsize = self.settings.get('label_font_size', 10)
 
-        with self.thmblock:
-            self.thumb_dict[thumbkey] = bnch
-            self.thumb_list.append(thumbkey)
+        # Shorten thumbnail label, if requested
+        label_length = self.settings.get('label_length', None)
+        label_cutoff = self.settings.get('label_cutoff', None)
 
-            sort_order = self.settings.get('sort_order', None)
-            if sort_order:
-                self.thumb_list.sort()
-                self.reorder_thumbs()
-                return
+        if label_length is not None:
+            if label_cutoff is not None:
+                thumbname = iohelper.shorten_name(thumbname, label_length,
+                                                  side=label_cutoff)
+            else:
+                thumbname = thumbname[:label_length]
 
-            self.w.thumbs.add_widget(vbox,
-                                     self.thumb_row_count, self.thumb_col_count)
+        with self.thmblock:
+            row, col = self.thumb_row_count, self.thumb_col_count
             self.thumb_col_count = (self.thumb_col_count + 1) % self.thumb_num_cols
             if self.thumb_col_count == 0:
                 self.thumb_row_count += 1
 
-        self._auto_scroll()
-        self.logger.debug("added thumb for %s" % (name))
+            xt, yt, xi, yi = self._calc_thumb_pos(row, col)
+            l2 = []
+            namelbl = canvas.dc.Text(xt, yt, thumbname, color=fg,
+                                     fontsize=fontsize, coord='data')
+            l2.append(namelbl)
+
+            image = canvas.dc.Image(xi, yi, imgwin, alpha=1.0,
+                                    linewidth=1, color='black', coord='data')
+            l2.append(image)
+
+            obj = canvas.dc.CompoundObject(*l2, coord='data')
+            obj.pickable = True
+            obj.opaque = True
+            obj.set_data(row=row, col=col)
+
+            bnch = Bunch.Bunch(widget=obj, image=image, info=info,
+                               namelbl=namelbl,
+                               chname=chname,
+                               thumbpath=thumbpath)
+
+            self.thumb_dict[thumbkey] = bnch
+            if thumbkey not in self.thumb_list:
+                self.thumb_list.append(thumbkey)
+
+            # set the load callback
+            obj.add_callback('pick-down',
+                             lambda *args: self.load_file(thumbkey, chname,
+                                                          info))
+            # set callbacks for tool tips
+            obj.add_callback('pick-enter', self.show_tt,
+                             thumbkey, chname, info, True)
+            obj.add_callback('pick-leave', self.show_tt,
+                             thumbkey, chname, info, False)
+
+            # thumb will be added to canvas later in reorder_thumbs()
 
-    def _auto_scroll(self):
+        sort_order = self.settings.get('sort_order', None)
+        if sort_order:
+            self.thumb_list.sort()
+
+        self.fv.gui_do_oneshot('thumbs-reorder', self.reorder_thumbs)
+
+        self.logger.debug("added thumb for %s" % (info.name))
+
+    def _auto_scroll(self, xi, yi):
         # force scroll to bottom of thumbs, if checkbox is set
         scrollp = self.w.auto_scroll.get_state()
         if scrollp:
-            self.fv.update_pending()
-            self.w.thumbs_scroll.scroll_to_end()
+            # override X parameter because we only want to scroll vertically
+            pan_x, pan_y = self.c_view.get_pan()
+            self.c_view.panset_xy(pan_x, yi)
 
     def clear_widget(self):
         """
         Clears the thumbnail display widget of all thumbnails, but does
         not remove them from the thumb_dict or thumb_list.
         """
-        with self.thmblock:
-            self.w.thumbs.remove_all()
+        canvas = self.c_view.get_canvas()
+        canvas.delete_all_objects()
+        self.c_view.redraw(whence=0)
 
     def reorder_thumbs(self):
         self.logger.debug("Reordering thumb grid")
+        canvas = self.c_view.get_canvas()
+
+        xi, yi = None, None
         with self.thmblock:
             self.clear_widget()
 
@@ -661,44 +875,63 @@ class Thumbs(GingaPlugin.GlobalPlugin):
             self.thumb_row_count = 0
             for thumbkey in self.thumb_list:
                 bnch = self.thumb_dict[thumbkey]
-                self.w.thumbs.add_widget(bnch.widget,
-                                         self.thumb_row_count, self.thumb_col_count)
+
+                row, col = self.thumb_row_count, self.thumb_col_count
                 self.thumb_col_count = ((self.thumb_col_count + 1) %
-                                        self.thumb_num_cols)
+                                      self.thumb_num_cols)
                 if self.thumb_col_count == 0:
                     self.thumb_row_count += 1
 
-        self._auto_scroll()
+                xt, yt, xi, yi = self._calc_thumb_pos(row, col)
+                bnch.namelbl.x, bnch.namelbl.y = xt, yt
+                bnch.image.x, bnch.image.y = xi, yi
+                bnch.widget.set_data(row=row, col=col)
+
+                canvas.add(bnch.widget, redraw=False)
+
+        if xi is not None:
+            xi += self.thumb_width
+            xm, ym, x_, y_ = self._calc_thumb_pos(0, 0)
+            self.c_view.set_limits([(xm, ym), (xi, yi)], coord='data')
+            self._auto_scroll(xi, yi)
+
+        self.c_view.redraw(whence=0)
+
         self.logger.debug("Reordering done")
 
-    def query_thumb(self, thumbkey, name, metadata):
+    def _mk_tooltip_text(self, metadata):
         result = []
         for kwd in self.keywords:
             try:
                 text = kwd + ': ' + str(metadata[kwd])
+
             except Exception as e:
-                self.logger.warning("Couldn't determine %s name: %s" % (
+                self.logger.debug("Couldn't get keyword '%s' value: %s" % (
                     kwd, str(e)))
                 text = "%s: N/A" % (kwd)
             result.append(text)
 
         return '\n'.join(result)
 
-    def _mk_context_menu(self, thumbkey, chname, name, path, image_future):
+    def _mk_context_menu(self, thumbkey, chname, info):
+        """NOTE: currently not used, but left here to be reincorporated
+        at some point.
+        """
         menu = Widgets.Menu()
         item = menu.add_name("Display")
         item.add_callback('activated',
                           lambda w: self.load_file(
-                              thumbkey, chname, name, path, image_future))
+                              thumbkey, chname, info.name, info.path,
+                              info.image_future))
         menu.add_separator()
         item = menu.add_name("Remove")
         item.add_callback('activated',
                           lambda w: self.fv.remove_image_by_name(
-                              chname, name, impath=path))
+                              chname, info.name, impath=info.path))
 
         return menu
 
-    def update_thumbnail(self, thumbkey, imgwin, name, metadata):
+    def update_thumbnail(self, thumbkey, thmb_image, metadata):
         with self.thmblock:
             try:
                 bnch = self.thumb_dict[thumbkey]
@@ -707,63 +940,23 @@ class Thumbs(GingaPlugin.GlobalPlugin):
                                   "thumbs" % (str(thumbkey)))
                 return
 
-            self.logger.info("updating thumbnail '%s'" % (name))
-            bnch.image._set_image(imgwin)
-            self.logger.debug("update finished.")
-
-    def add_image_info_cb(self, viewer, channel, info):
+            info = bnch.info
+            # Get any previously stored thumb information in the image info
+            thumb_extra = info.setdefault('thumb_extras', Bunch.Bunch())
 
-        save_thumb = self.settings.get('cache_thumbs', False)
+            # Update the tooltip, in case of new or changed metadata
+            text = self._mk_tooltip_text(metadata)
+            thumb_extra.tooltip = text
 
-        # Do we already have this thumb loaded?
-        chname = channel.name
-        thumbkey = self.get_thumb_key(chname, info.name, info.path)
-        thumbpath = self.get_thumbpath(info.path)
+            self.logger.debug("updating thumbnail '%s'" % (info.name))
+            # TODO: figure out why set_image() causes corruption of the
+            # redraw here.  Instead we force a manual redraw.
+            #bnch.image.set_image(thmb_image)
+            bnch.image.image = thmb_image
+            thumb_extra.rgbimg = thmb_image
 
-        with self.thmblock:
-            try:
-                bnch = self.thumb_dict[thumbkey]
-                # if these are not equal then the mtime must have
-                # changed on the file, better reload and regenerate
-                if bnch.thumbpath == thumbpath:
-                    return
-            except KeyError:
-                pass
-
-        # Is there a cached thumbnail image on disk we can use?
-        thmb_image = RGBImage.RGBImage()
-        loaded = False
-        if (thumbpath is not None) and os.path.exists(thumbpath):
-            #save_thumb = False
-            try:
-                # try to load the thumbnail image
-                thmb_image.load_file(thumbpath)
-                # make sure name is consistent
-                thmb_image.set(name=info.name)
-                loaded = True
-
-            except Exception as e:
-                self.logger.warning("Error loading thumbnail: %s" % (str(e)))
-
-        if not loaded:
-            # no luck loading thumbnail, try to load the full image
-            try:
-                thmb_image = channel.get_loaded_image(info.name)
-
-            except KeyError:
-                self.logger.info("image not in memory; using placeholder")
-
-                # load a plcaeholder image
-                tmp_path = os.path.join(icondir, 'fits.png')
-                thmb_image = RGBImage.RGBImage()
-                thmb_image.load_file(tmp_path)
-                # make sure name is consistent
-                thmb_image.set(name=info.name, path=None)
-
-        self.fv.gui_do(self._make_thumb, chname, thmb_image, info.name,
-                       info.path, thumbkey, info.image_future,
-                       save_thumb=save_thumb,
-                       thumbpath=thumbpath)
+            self.c_view.redraw(whence=0)
+            self.logger.debug("update finished.")
 
 
     def __str__(self):
['ginga/rv/plugins/Catalogs.py', 'ginga/rv/plugins/Header.py', 'ginga/rv/plugins/Mosaic.py', 'ginga/GingaPlugin.py', 'ginga/rv/plugins/Pan.py', 'ginga/examples/configs/plugin_Thumbs.cfg', 'ginga/rv/Control.py', 'ginga/rv/plugins/Thumbs.py', 'ginga/rv/Channel.py', 'ginga/rv/plugins/Contents.py', 'ginga/canvas/types/astro.py', 'ginga/rv/plugins/ChangeHistory.py']