text: # This is open-source software licensed under a BSD license.
  # Please see the file LICENSE.txt for details.
  """
  The ``Contents`` plugin provides a table of contents-like interface for all
  the images viewed since the program was started.  Unlike ``Thumbs``,
  ``Contents`` is sorted by channel.  The contents also shows some configurable
  metadata from the image.
  
  **Plugin Type: Global**
  
  ``Contents`` is a global plugin.  Only one instance can be opened.
  
  **Usage**
  
  Click on a column heading to sort the table by that column;
  Click again to sort the other way.
  
  .. note:: The columns and their values are drawn from the FITS header,
            if applicable.
            This can be customized by setting the "columns" parameter in
            the "plugin_Contents.cfg" settings file.
  
  The active image in the currently focused channel will normally be
  highlighted. Double-click on an image will force that image to be
  shown in the associated channel. Single-click on any image to
  activate the buttons at the bottom of the UI:
  
  * "Display": Make the image the active image.
  * "Move": Move the image to another channel.
  * "Copy": Copy the image to another channel.
  * "Remove": Remove the image from the channel.
  
  If "Move" or "Copy" is done on an image that has been modified in Ginga
  (which would have an entry under ``ChangeHistory``, if used), the
  modification history will be retained as well. Removing an image from
  a channel destroys any unsaved changes.
  
  """
  from ginga.util.six.moves import map
  
  from ginga import GingaPlugin
  from ginga.misc import Bunch
  
  from ginga.gw import Widgets
  
  __all__ = ['Contents']
  
  
  class Contents(GingaPlugin.GlobalPlugin):
  
      def __init__(self, fv):
          # superclass defines some variables for us, like logger
          super(Contents, self).__init__(fv)
  
          columns = [('Name', 'NAME'), ('Object', 'OBJECT'),
                     ('Date', 'DATE-OBS'), ('Time UT', 'UT'),
                     ('Modified', 'MODIFIED')]
  
          prefs = self.fv.get_preferences()
          self.settings = prefs.create_category('plugin_Contents')
          self.settings.add_defaults(columns=columns,
                                     always_expand=True,
                                     highlight_tracks_keyboard_focus=True,
                                     color_alternate_rows=True,
                                     row_font_color='green',
                                     max_rows_for_col_resize=100)
          self.settings.load(onError='silent')
  
          # For table-of-contents pane
          self.name_dict = Bunch.caselessDict()
          # TODO: this ought to be customizable by channel
          self.columns = self.settings.get('columns', columns)
          self.treeview = None
          # paths of highlighted entries, by channel
          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('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)
  
          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('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 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, bnch = res[0]
  
          if 'path' not 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))
  
          self.fv.switch_name(chname, imname, path=path,
                              image_future=bnch.image_future)
  
      def get_info(self, chname, name, image, info):
          path = info.get('path', None)
          future = info.get('image_future', None)
  
          bnch = Bunch.Bunch(CHNAME=chname, imname=name, path=path,
                             image_future=future)
  
          # Get header keywords of interest
          if image is not None:
              header = image.get_header()
          else:
              header = {}
  
          for hdr, key in self.columns:
              bnch[key] = str(header.get(key, 'N/A'))
  
          # name should always be available
          bnch.NAME = name
  
          # Modified timestamp will be set if image data is modified
          timestamp = info.time_modified
          if timestamp is not None:
              # Z: Zulu time, GMT, UTC
              timestamp = timestamp.strftime('%Y-%m-%d %H:%M:%SZ')
          bnch.MODIFIED = timestamp
  
          return bnch
  
      def recreate_toc(self):
          self.logger.debug("Recreating table of contents...")
          self.treeview.set_tree(self.name_dict)
  
          # re-highlight as necessary
          if self.highlight_tracks_keyboard_focus:
              new_highlight = self._hl_path
          else:
              new_highlight = set([])
              for chname in self.name_dict:
                  channel = self.fv.get_channel_info(chname)
                  new_highlight |= channel.extdata.contents_old_highlight
          self.update_highlights(set([]), new_highlight)
  
          # Resize column widths
          n_rows = sum(map(len, self.name_dict.values()))
          if n_rows < self.settings.get('max_rows_for_col_resize', 100):
              self.treeview.set_optimal_column_widths()
              self.logger.debug("Resized columns for {0} row(s)".format(n_rows))
  
      def is_in_contents(self, chname, imname):
          if chname not in self.name_dict:
              return False
  
          file_dict = self.name_dict[chname]
          if imname not in file_dict:
              return False
  
          return True
  
      def add_image_cb(self, viewer, chname, image, image_info):
          if not self.gui_up:
              return False
  
          name = image_info.name
          self.logger.debug("name=%s" % (name))
  
          if image is not None:
              nothumb = image.get('nothumb', False)
              if nothumb:
                  return
  
          bnch = self.get_info(chname, name, image, image_info)
  
          if chname not in self.name_dict:
              # channel does not exist yet in contents
              # Note: this typically shouldn't happen, because add_channel_cb()
              # will have added an empty dict
              file_dict = {}
              self.name_dict[chname] = file_dict
          else:
              file_dict = self.name_dict[chname]
  
          if name not in file_dict:
              # new image
              file_dict[name] = bnch
          else:
              # old image
              file_dict[name].update(bnch)
  
          # TODO: either make add_tree() merge updates or make an
          #    update_tree() method--shouldn't need to recreate entire
          #    tree, just add new entry and possibly rehighlight
          ## tree_dict = { chname: { name: bnch } }
          ## self.treeview.add_tree(tree_dict)
          self.recreate_toc()
  
          self.logger.debug("%s added to Contents" % (name))
  
      def add_image_info_cb(self, viewer, channel, image_info):
          """Almost the same as add_image_cb(), except that the image
          may not be loaded in memory.
          """
          chname = channel.name
          name = image_info.name
          self.logger.debug("name=%s" % (name))
  
          # Updates of any extant information
          try:
              image = channel.get_loaded_image(name)
          except KeyError:
              # images that are not yet loaded will show "N/A" for keywords
              image = None
  
          self.add_image_cb(viewer, chname, image, image_info)
  
      def remove_image_cb(self, viewer, chname, name, path):
          if not self.gui_up:
              return False
  
          if chname not in self.name_dict:
              return
  
          file_dict = self.name_dict[chname]
  
          if name not in file_dict:
              return
  
          del file_dict[name]
  
          # Unhighlight
          channel = self.fv.get_channel_info(chname)
          key = (chname, name)
          self._hl_path.discard(key)
          channel.extdata.contents_old_highlight.discard(key)
  
          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([])
          self.recreate_toc()
  
      def add_channel_cb(self, viewer, channel):
          """Called when a channel is added from the main interface.
          Parameter is a channel (a Channel object)."""
          chname = channel.name
  
          # add old highlight set to channel external data
          channel.extdata.setdefault('contents_old_highlight', set([]))
  
          # Add the channel to the treeview
          file_dict = {}
          self.name_dict.setdefault(chname, file_dict)
  
          if not self.gui_up:
              return False
  
          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)."""
          chname = channel.name
          del self.name_dict[chname]
  
          # Unhighlight
          un_hilite_set = set([])
          for path in self._hl_path:
              if path[0] == chname:
                  un_hilite_set.add(path)
          self._hl_path -= un_hilite_set
  
          if not self.gui_up:
              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'))
  
      def _highlight_path(self, hl_path, tf):
          """Highlight or unhighlight a single entry.
  
          Examples
          --------
          >>> hl_path = self._get_hl_key(chname, image)
          >>> self._highlight_path(hl_path, True)
  
          """
          fc = self.settings.get('row_font_color', 'green')
  
          try:
              self.treeview.highlight_path(hl_path, tf, font_color=fc)
          except Exception as e:
              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):
          """Unhighlight the entries represented by ``old_highlight_set``
          and highlight the ones represented by ``new_highlight_set``.
  
          Both are sets of keys.
  
          """
          un_hilite_set = old_highlight_set - new_highlight_set
          re_hilite_set = new_highlight_set - old_highlight_set
  
          # unhighlight entries that should NOT be highlighted any more
          for key in un_hilite_set:
              self._highlight_path(key, False)
  
          # highlight new entries that should be
          for key in re_hilite_set:
              self._highlight_path(key, True)
  
      def redo(self, channel, image):
          """This method is called when an image is set in a channel."""
  
          imname = image.get('name', 'none')
          chname = channel.name
          # is image in contents tree yet?
          in_contents = self.is_in_contents(chname, imname)
  
          # get old highlighted entries for this channel -- will be
          # an empty set or one key
          old_highlight = channel.extdata.contents_old_highlight
  
          # calculate new highlight keys -- again, an empty set or one key
          if image is not None:
              key = self._get_hl_key(chname, image)
              new_highlight = set([key])
          else:
              # no image has the focus
              new_highlight = set([])
  
          # Only highlights active image in the current channel
          if self.highlight_tracks_keyboard_focus:
              if in_contents:
                  self.update_highlights(self._hl_path, new_highlight)
              self._hl_path = new_highlight
  
          # Highlight all active images in all channels
          else:
              if in_contents:
                  self.update_highlights(old_highlight, new_highlight)
              channel.extdata.contents_old_highlight = new_highlight
  
          return True
  
      def focus_cb(self, viewer, channel):
          chname = channel.name
          image = channel.get_current_image()
  
          if image is not None:
              key = self._get_hl_key(chname, image)
              new_highlight = set([key])
          else:
              # no image has the focus
              new_highlight = set([])
  
          if self.highlight_tracks_keyboard_focus:
              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'
  
  
  # Append module docstring with config doc for auto insert by Sphinx.
  from ginga.util.toolbox import generate_cfg_example  # noqa
  if __doc__ is not None:
      __doc__ += generate_cfg_example('plugin_Contents', package='ginga')
  
  # END
  