示例#1
0
 def __init__(self, scraper, book, series_refs, search_terms_s):
    ''' 
    Initializes this form.
    
    'scraper' -> the currently running ScrapeEngine
    'book' -> the ComicBook being scraped
    'series_refs' -> set or list containing the SeriesRefs to display
    'search_terms_s' -> the user's search string that found the series models
    '''
    
    # the the shared global configuration
    self.__config = scraper.config
    
    # a list of SeriesRef objects that back this form; one ref per table
    # row, where each SeriesRef represents a series the user can pick
    self.__series_refs = list(series_refs)
    
    # the MatchScore object that we use to compute series match scores
    self.__matchscore = MatchScore()
    
    # true when the user is pressing the control key, false otherwise
    self.__pressing_controlkey = False;
    
    # the 'ok' button for this dialog
    self.__ok_button = None
    
    # the 'skip' button for this dialog
    self.__skip_button = None
    
    # the 'show issues' button for this dialog
    self.__issues_button = None
    
    # the table that displays series (on per row) for the user to pick from
    self.__table = None
    
    # IssueCoverPanel that shows cover art for the current selected SeriesRef
    self.__coverpanel = None
    
    # the index (in self.__series_refs) of the currently selected SeriesRef
    self.__chosen_index = None
    
    
    
    if len(series_refs) <= 0:
       raise Exception("do not invoke the SeriesForm with no series!")
    CVForm.__init__(self, scraper.comicrack.MainWindow, "seriesformLocation")
    self.__build_gui(book, search_terms_s);
    scraper.cancel_listeners.append(self.Close)
示例#2
0
def __find_best_series(book, config):
    ''' 
   Queries the databse to find a best guess for a series matching the given
   ComicBook, based on its name, year, issue number, and other text attributes.
   
   Returns SeriesRef if a reasonable guess was found, or None if one wasn't.
   '''

    # 1. obtain SeriesRefs for this book, removing some as dictated by prefs
    series_refs = db.query_series_refs(book.series_s,
                                       config.ignored_searchterms_sl)
    series_refs = dbutils.filter_series_refs(series_refs,
                                             config.ignored_publishers_sl,
                                             config.ignored_before_year_n,
                                             config.ignored_after_year_n,
                                             config.never_ignore_threshold_n)

    # 2. obtain the first, second, and third best matching SeriesRefs for the
    #    given book, if there are any.
    primary = None
    secondary = None
    tertiary = None
    if len(series_refs) > 0:
        mscore = MatchScore()

        def find_best_score(refs):
            return reduce(
                lambda x, y: x if mscore.compute_n(book, x) >= mscore.
                compute_n(book, y) else y, refs) if refs else None

        primary = find_best_score(series_refs)
        if primary:
            series_refs.remove(primary)
            secondary = find_best_score(series_refs)
            if secondary:
                series_refs.remove(secondary)
                tertiary = find_best_score(series_refs)

        # 3. if our book is the first (or unknown) issue, figure out if the best
        #    matching series has a similar cover to the second or third best.
        #    if it does, we're probably dealing with a trade paperback and a
        #    regular issue, and we can't find the best series reliably, so we bail
        is_first_issue = (lambda i : not i or \
           (utils.is_number(i) and float(i)==1.0))(book.issue_num_s)
        if is_first_issue and primary and secondary:
            too_similar = False
            SIMILARITY_THRESHOLD = __MATCH_THRESHOLD - 0.10
            hash1 = __get_remote_hash(primary)
            hash2 = __get_remote_hash(secondary)
            if imagehash.similarity(hash1, hash2) > SIMILARITY_THRESHOLD:
                too_similar = True
            elif tertiary:
                hash3 = __get_remote_hash(tertiary)
                if imagehash.similarity(hash1, hash3) > SIMILARITY_THRESHOLD:
                    too_similar = True
            primary = None if too_similar else primary

    return primary
示例#3
0
class SeriesForm(CVForm):
    '''
   This class is a popup, modal dialog that displays all of the Comic Book
   series that match a particular search string.  The series are shown in a 
   table, which the user can navigate through, browsing the cover art for 
   the first issue of each series.  Once the user has selected the series that
   matches the comic that she is scraping, she clicks the ok button to close
   this dialog and continue scraping her comic using the identified SeriesRef.
   '''

    #===========================================================================
    def __init__(self, scraper, book, series_refs, search_terms_s):
        ''' 
      Initializes this form.
      
      'scraper' -> the currently running ScrapeEngine
      'book' -> the ComicBook being scraped
      'series_refs' -> set or list containing the SeriesRefs to display
      'search_terms_s' -> the user's search string that found the series models
      '''

        # the the shared global configuration
        self.__config = scraper.config

        # a list of SeriesRef objects that back this form; one ref per table
        # row, where each SeriesRef represents a series the user can pick
        self.__series_refs = list(series_refs)

        # the MatchScore object that we use to compute series match scores
        self.__matchscore = MatchScore()

        # true when the user is pressing the control key, false otherwise
        self.__pressing_controlkey = False

        # the 'ok' button for this dialog
        self.__ok_button = None

        # the 'skip' button for this dialog
        self.__skip_button = None

        # the 'show issues' button for this dialog
        self.__issues_button = None

        # the table that displays series (on per row) for the user to pick from
        self.__table = None

        # IssueCoverPanel that shows cover art for the current selected SeriesRef
        self.__coverpanel = None

        # the index (in self.__series_refs) of the currently selected SeriesRef
        self.__chosen_index = None

        if len(series_refs) <= 0:
            raise Exception("do not invoke the SeriesForm with no series!")
        CVForm.__init__(self, scraper.comicrack.MainWindow,
                        "seriesformLocation")
        self.__build_gui(book, search_terms_s)
        scraper.cancel_listeners.append(self.Close)

    #===========================================================================
    def __build_gui(self, book, search_terms_s):
        ''' Constructs and initializes the gui for this form. '''

        # 1. --- build each gui component
        self.__ok_button = self.__build_okbutton()
        self.__skip_button = self.__build_skipbutton()
        search_button = self.__build_searchbutton()
        self.__issues_button = self.__build_issuesbutton()
        label = self.__build_label(search_terms_s, len(self.__series_refs))
        self.__table = self.__build_table(self.__series_refs, book,
                                          self.__ok_button)
        self.__coverpanel = self.__build_coverpanel(book)

        # 2. --- configure this form, and add all the gui components to it
        self.AutoScaleMode = AutoScaleMode.Font
        self.ClientSize = Size(730, 395)
        self.Text = i18n.get("SeriesFormTitle")
        self.FormClosed += self.__form_closed_fired
        self.KeyPreview = True
        self.KeyDown += self.__key_was_pressed
        self.KeyUp += self.__key_was_released
        self.Deactivate += self.__was_deactivated

        self.Controls.Add(label)
        self.Controls.Add(self.__table)
        self.Controls.Add(self.__ok_button)
        self.Controls.Add(self.__skip_button)
        self.Controls.Add(search_button)
        self.Controls.Add(self.__issues_button)
        self.Controls.Add(self.__coverpanel)  # must be added LAST

        # 3. --- define the keyboard focus tab traversal ordering
        self.__ok_button.TabIndex = 1
        self.__skip_button.TabIndex = 2
        search_button.TabIndex = 3
        self.__issues_button.TabIndex = 4
        self.__coverpanel.TabIndex = 5
        self.__table.TabIndex = 6

        # 4. --- make sure the UI goes into a good initial state
        self.Shown += self.__change_table_selection_fired

    # ==========================================================================
    def __build_table(self, series_refs, book, enter_button):
        ''' 
      Builds and returns the table for this form.
      'series_refs' -> a list with one SeriesRef object for each found series
      'book' -> the ComicBook being scraped
      'enter_button' -> the button to "press" if the user hits enter
      '''

        # 1. --- configure the table itself
        table = ButtonDataGridView(enter_button)
        table.AllowUserToOrderColumns = True
        table.SelectionMode = DataGridViewSelectionMode.FullRowSelect
        table.MultiSelect = False
        table.ReadOnly = True
        table.RowHeadersVisible = False
        table.AllowUserToAddRows = False
        table.AllowUserToResizeRows = False
        table.AllowUserToResizeColumns = False
        table.DefaultCellStyle.NullValue = "--"

        table.Location = Point(10, 60)
        table.Size = Size(500, 290) \
           if self.__config.show_covers_b else Size(710, 290)

        # 2. --- build columns
        table.ColumnCount = 7

        table.Columns[0].Name = i18n.get("SeriesFormSeriesCol")
        table.Columns[0].DefaultCellStyle.Alignment =\
           DataGridViewContentAlignment.MiddleLeft
        table.Columns[0].Resizable = DataGridViewTriState. True
        table.Columns[0].FillWeight = 200
        table.Columns[0].AutoSizeMode = \
           DataGridViewAutoSizeColumnMode.Fill

        table.Columns[1].Name = i18n.get("SeriesFormYearCol")
        table.Columns[1].DefaultCellStyle.Alignment =\
           DataGridViewContentAlignment.MiddleCenter
        table.Columns[1].Resizable = DataGridViewTriState. True
        table.Columns[1].AutoSizeMode =\
           DataGridViewAutoSizeColumnMode.AllCells

        table.Columns[2].Name = i18n.get("SeriesFormIssuesCol")
        table.Columns[2].DefaultCellStyle.Alignment =\
           DataGridViewContentAlignment.MiddleCenter
        table.Columns[2].Resizable = DataGridViewTriState. True
        table.Columns[2].AutoSizeMode =\
           DataGridViewAutoSizeColumnMode.AllCells

        table.Columns[3].Name = i18n.get("SeriesFormPublisherCol")
        table.Columns[3].DefaultCellStyle.Alignment =\
           DataGridViewContentAlignment.MiddleLeft
        table.Columns[3].Resizable = DataGridViewTriState. True
        table.Columns[3].AutoSizeMode =\
           DataGridViewAutoSizeColumnMode.Fill

        table.Columns[4].Name = "ID"
        table.Columns[4].Visible = False
        table.Columns[4].DefaultCellStyle.Alignment =\
           DataGridViewContentAlignment.MiddleCenter
        table.Columns[4].AutoSizeMode =\
           DataGridViewAutoSizeColumnMode.AllCells

        table.Columns[5].Name = "Match"
        table.Columns[5].Visible = False
        table.Columns[5].DefaultCellStyle.Alignment =\
           DataGridViewContentAlignment.MiddleCenter
        table.Columns[5].AutoSizeMode =\
           DataGridViewAutoSizeColumnMode.AllCells

        table.Columns[6].Name = "Model ID"
        table.Columns[6].Visible = False
        table.Columns[6].DefaultCellStyle.Alignment =\
           DataGridViewContentAlignment.MiddleCenter
        table.Columns[6].AutoSizeMode =\
           DataGridViewAutoSizeColumnMode.AllCells

        # 3. --- copy model data into the table, each series is a row
        for i in range(len(series_refs)):
            table.Rows.Add()
            ref = series_refs[i]
            table.Rows[i].Cells[0].Value = ref.series_name_s
            if ref.volume_year_n >= 0:
                table.Rows[i].Cells[1].Value = ref.volume_year_n
            table.Rows[i].Cells[2].Value = ref.issue_count_n
            table.Rows[i].Cells[3].Value = ref.publisher_s
            table.Rows[i].Cells[4].Value = ref.series_key
            table.Rows[i].Cells[5].Value = self.__matchscore.compute_n(
                book, ref)
            table.Rows[i].Cells[6].Value = i

        # 4. --- sort on the "match" colum
        table.Sort(table.Columns[5], ListSortDirection.Descending)
        table.SelectionChanged += self.__change_table_selection_fired
        return table

    # ==========================================================================
    def __build_okbutton(self):
        ''' builds and returns the ok button for this form '''

        button = Button()
        button.DialogResult = DialogResult.OK
        button.Location = Point(15, 362)
        button.Size = Size(90, 24)
        button.Text = i18n.get("SeriesFormOK")
        return button

    # ==========================================================================
    def __build_skipbutton(self):
        ''' builds and return the skip button for this form '''

        button = Button()
        button.DialogResult = DialogResult.Ignore
        button.Location = Point(110, 362)
        button.Size = Size(90, 24)
        button.Text = i18n.get("SeriesFormSkip")
        return button

    # ==========================================================================
    def __build_searchbutton(self):
        ''' builds and return the 'search again' button for this form '''

        button = Button()
        button.DialogResult = DialogResult.Retry
        button.Location = Point(275, 362) \
           if self.__config.show_covers_b else Point(485, 362)
        button.Size = Size(115, 24)
        button.Text = i18n.get("SeriesFormAgain")
        return button

    # ==========================================================================
    def __build_issuesbutton(self):
        ''' builds and return the 'show issues' button for this form '''

        button = Button()
        button.DialogResult = DialogResult.Yes
        button.Location = Point(395, 362) \
           if self.__config.show_covers_b else Point(605, 362)
        button.Size = Size(115, 24)
        button.Text = i18n.get("SeriesFormIssues")
        return button

    # ==========================================================================
    def __build_label(self, search_terms_s, num_matches_n):
        ''' 
      Builds and return the text label for this form.
      'search_terms_s' -> user's search string that was used to find series
      'num_matches_n' -> number of series (table rows) the user's search matched
      '''

        label = Label()
        label.UseMnemonic = False
        label.Location = Point(10, 20)
        label.Size = Size(480, 40)
        if num_matches_n > 1:
            label.Text = i18n.get("SeriesFormChooseText")\
               .format(search_terms_s, num_matches_n )
        else:
            label.Text = i18n.get("SeriesFormConfirmText").format(
                search_terms_s)
        return label

    # ==========================================================================
    def __build_coverpanel(self, book):
        ''' 
      Builds and returns the cover image PictureBox for this form.
      'book' -> the ComicBook being scraped
      '''
        panel = IssueCoverPanel(self.__config, -9991 \
           if self.__config.force_series_art_b else book.issue_num_s)
        panel.Location = Point(523, 30)
        # panel size is determined by the panel itself

        if self.__config.show_covers_b:
            panel.Show()
        else:
            panel.Hide()
        return panel

    # ==========================================================================
    def show_form(self):
        '''
      Displays this form, blocking until the user closes it.  When it is closed,
      it will return a SeriesFormResult describing how it was closed, and any
      SeriesRef that may have been chosen when it was closed. 
      '''

        dialogAnswer = self.ShowDialog(self.Owner)  # blocks

        if dialogAnswer == DialogResult.OK:
            series = self.__series_refs[self.__chosen_index]
            result = SeriesFormResult("OK", series)
            alt_choice = self.__coverpanel.get_alt_issue_cover_choice()
            if alt_choice:
                issue_ref, image_ref = alt_choice
                # the user chose a non-default cover image for this issue.
                # we'll store that choice in the global "session data map",
                # in case any other part of the program wants to use it.
                alt_cover_key = sstr(issue_ref.issue_key) + "-altcover"
                self.__config.session_data_map[alt_cover_key] = image_ref
        elif dialogAnswer == DialogResult.Yes:
            series = self.__series_refs[self.__chosen_index]
            result = SeriesFormResult("SHOW", series)
        elif dialogAnswer == DialogResult.Cancel:
            result = SeriesFormResult("CANCEL")
        elif dialogAnswer == DialogResult.Ignore:
            if self.ModifierKeys == Keys.Control:
                result = SeriesFormResult("PERMSKIP")
            else:
                result = SeriesFormResult("SKIP")
        elif dialogAnswer == DialogResult.Retry:
            result = SeriesFormResult("SEARCH")
        else:
            raise Exception()

        return result

    #===========================================================================
    def __form_closed_fired(self, sender, args):
        ''' this method is called whenever this SeriesForm is closed. '''

        self.__table.Dispose()
        self.__coverpanel.free()
        self.Closed -= self.__form_closed_fired

    #===========================================================================
    def __change_table_selection_fired(self, sender, args):
        ''' this method is called whenever the table's selected row changes. '''

        # update __chosen_index (eventually used as this dialog's return value)
        # and then also use it to update the displayed cover image.
        selected_rows = self.__table.SelectedRows
        if selected_rows.Count == 1:
            self.__chosen_index = selected_rows[0].Cells[6].Value
            self.__coverpanel.set_ref(self.__series_refs[self.__chosen_index])
        else:
            self.__chosen_index = None
            self.__coverpanel.set_ref(None)

        # don't let the user click 'ok' or 'show issue' if no row is selected!
        self.__ok_button.Enabled = selected_rows.Count == 1
        self.__issues_button.Enabled = selected_rows.Count == 1

    #===========================================================================
    def __key_was_pressed(self, sender, args):
        ''' Called whenever the user presses any key on this form. '''

        # highlight the skip button whenever the user presses control key
        if args.KeyCode == Keys.ControlKey and not self.__pressing_controlkey:
            self.__pressing_controlkey = True
            self.__skip_button.Text = "- " + i18n.get("SeriesFormSkip") + " -"

    #===========================================================================
    def __key_was_released(self, sender, args):
        ''' Called whenever the user releases any key on this form. '''

        # unhighlight the skip button bold whenever the user releases control key
        if args.KeyCode == Keys.ControlKey:
            self.__pressing_controlkey = False
            self.__skip_button.Text = i18n.get("SeriesFormSkip")

    #===========================================================================
    def __was_deactivated(self, sender, args):
        ''' Called whenever this form gets deactivated, for any reason '''

        # unhighlight the skip button bold whenever we deactivate
        if self.__pressing_controlkey:
            self.__pressing_controlkey = False
            self.__skip_button.Text = i18n.get("SeriesFormSkip")
class SeriesForm(CVForm):
   '''
   This class is a popup, modal dialog that displays all of the Comic Book
   series that match a particular search string.  The series are shown in a 
   table, which the user can navigate through, browsing the cover art for 
   the first issue of each series.  Once the user has selected the series that
   matches the comic that she is scraping, she clicks the ok button to close
   this dialog and continue scraping her comic using the identified SeriesRef.
   '''

   #===========================================================================
   def __init__(self, scraper, book, series_refs, search_terms_s):
      ''' 
      Initializes this form.
      
      'scraper' -> the currently running ScrapeEngine
      'book' -> the ComicBook being scraped
      'series_refs' -> set or list containing the SeriesRefs to display
      'search_terms_s' -> the user's search string that found the series models
      '''
      
      # the the shared global configuration
      self.__config = scraper.config
      
      # a list of SeriesRef objects that back this form; one ref per table
      # row, where each SeriesRef represents a series the user can pick
      self.__series_refs = list(series_refs)
      
      # the MatchScore object that we use to compute series match scores
      self.__matchscore = MatchScore()
      
      # true when the user is pressing the control key, false otherwise
      self.__pressing_controlkey = False;
      
      # the 'ok' button for this dialog
      self.__ok_button = None
      
      # the 'skip' button for this dialog
      self.__skip_button = None
      
      # the 'show issues' button for this dialog
      self.__issues_button = None
      
      # the table that displays series (on per row) for the user to pick from
      self.__table = None
      
      # IssueCoverPanel that shows cover art for the current selected SeriesRef
      self.__coverpanel = None
      
      # the index (in self.__series_refs) of the currently selected SeriesRef
      self.__chosen_index = None
      
      
      
      if len(series_refs) <= 0:
         raise Exception("do not invoke the SeriesForm with no series!")
      CVForm.__init__(self, scraper.comicrack.MainWindow, "seriesformLocation")
      self.__build_gui(book, search_terms_s);
      scraper.cancel_listeners.append(self.Close)
      
      
   #===========================================================================   
   def __build_gui(self, book, search_terms_s):
      ''' Constructs and initializes the gui for this form. '''
      
      # 1. --- build each gui component
      self.__ok_button = self.__build_okbutton()
      self.__skip_button = self.__build_skipbutton()
      search_button = self.__build_searchbutton()
      self.__issues_button = self.__build_issuesbutton()
      label = self.__build_label(search_terms_s, len(self.__series_refs)) 
      self.__table = self.__build_table(
         self.__series_refs, book, self.__ok_button)
      self.__coverpanel = self.__build_coverpanel(book)

      # 2. --- configure this form, and add all the gui components to it
      self.AutoScaleMode = AutoScaleMode.Font
      self.ClientSize = Size(730, 395)
      self.Text = i18n.get("SeriesFormTitle")
      self.FormClosed += self.__form_closed_fired
      self.KeyPreview = True;
      self.KeyDown += self.__key_was_pressed
      self.KeyUp += self.__key_was_released
      self.Deactivate += self.__was_deactivated
      
      self.Controls.Add (label)
      self.Controls.Add(self.__table)
      self.Controls.Add (self.__ok_button)
      self.Controls.Add (self.__skip_button)
      self.Controls.Add (search_button)
      self.Controls.Add (self.__issues_button)
      self.Controls.Add(self.__coverpanel) # must be added LAST
      
      # 3. --- define the keyboard focus tab traversal ordering
      self.__ok_button.TabIndex = 1
      self.__skip_button.TabIndex = 2
      search_button.TabIndex = 3
      self.__issues_button.TabIndex = 4
      self.__coverpanel.TabIndex = 5
      self.__table.TabIndex = 6
      
      # 4. --- make sure the UI goes into a good initial state
      self.Shown += self.__change_table_selection_fired



   # ==========================================================================
   def __build_table(self, series_refs, book, enter_button):
      ''' 
      Builds and returns the table for this form.
      'series_refs' -> a list with one SeriesRef object for each found series
      'book' -> the ComicBook being scraped
      'enter_button' -> the button to "press" if the user hits enter
      '''
      
      # 1. --- configure the table itself
      table = ButtonDataGridView(enter_button) 
      table.AllowUserToOrderColumns = True
      table.SelectionMode = DataGridViewSelectionMode.FullRowSelect
      table.MultiSelect = False
      table.ReadOnly = True
      table.RowHeadersVisible = False
      table.AllowUserToAddRows = False
      table.AllowUserToResizeRows = False
      table.AllowUserToResizeColumns = False
      table.DefaultCellStyle.NullValue = "--"
      table.AutoResizeColumns

      table.Location = Point(10, 60)
      table.Size = Size(500, 290) \
         if self.__config.show_covers_b else Size(710, 290)

      # 2. --- build columns
      table.ColumnCount = 7
      
      table.Columns[0].Name = i18n.get("SeriesFormSeriesCol")
      table.Columns[0].DefaultCellStyle.Alignment =\
         DataGridViewContentAlignment.MiddleLeft
      table.Columns[0].Resizable = DataGridViewTriState.True
      table.Columns[0].FillWeight = 200
      table.Columns[0].AutoSizeMode = \
         DataGridViewAutoSizeColumnMode.Fill
      
      table.Columns[1].Name = i18n.get("SeriesFormYearCol")
      table.Columns[1].DefaultCellStyle.Alignment =\
         DataGridViewContentAlignment.MiddleCenter
      table.Columns[1].Resizable = DataGridViewTriState.True
      table.Columns[1].AutoSizeMode =\
         DataGridViewAutoSizeColumnMode.AllCells
      
      table.Columns[2].Name = i18n.get("SeriesFormIssuesCol")
      table.Columns[2].DefaultCellStyle.Alignment =\
         DataGridViewContentAlignment.MiddleCenter
      table.Columns[2].Resizable = DataGridViewTriState.True
      table.Columns[2].AutoSizeMode =\
         DataGridViewAutoSizeColumnMode.AllCells
         
      table.Columns[3].Name = i18n.get("SeriesFormPublisherCol")
      table.Columns[3].DefaultCellStyle.Alignment =\
         DataGridViewContentAlignment.MiddleLeft
      table.Columns[3].Resizable = DataGridViewTriState.True
      table.Columns[3].AutoSizeMode =\
         DataGridViewAutoSizeColumnMode.Fill
         
      table.Columns[4].Name = "ID"
      table.Columns[4].Visible = False
      table.Columns[4].DefaultCellStyle.Alignment =\
         DataGridViewContentAlignment.MiddleCenter
      table.Columns[4].AutoSizeMode =\
         DataGridViewAutoSizeColumnMode.AllCells
         
      table.Columns[5].Name = "Match"
      table.Columns[5].Visible = False
      table.Columns[5].DefaultCellStyle.Alignment =\
         DataGridViewContentAlignment.MiddleCenter
      table.Columns[5].AutoSizeMode =\
         DataGridViewAutoSizeColumnMode.AllCells
         
      table.Columns[6].Name = "Model ID"
      table.Columns[6].Visible = False
      table.Columns[6].DefaultCellStyle.Alignment =\
         DataGridViewContentAlignment.MiddleCenter
      table.Columns[6].AutoSizeMode =\
         DataGridViewAutoSizeColumnMode.AllCells

      # 3. --- copy model data into the table, each series is a row
      for i in range(len(series_refs)):
         table.Rows.Add()
         ref = series_refs[i] 
         table.Rows[i].Cells[0].Value = ref.series_name_s
         if ref.volume_year_n >= 0:
            table.Rows[i].Cells[1].Value = ref.volume_year_n
         table.Rows[i].Cells[2].Value = ref.issue_count_n
         table.Rows[i].Cells[3].Value = ref.publisher_s
         table.Rows[i].Cells[4].Value = ref.series_key
         table.Rows[i].Cells[5].Value = self.__matchscore.compute_n(book, ref)
         table.Rows[i].Cells[6].Value = i

      # 4. --- sort on the "match" colum
      table.Sort( table.Columns[5], ListSortDirection.Descending )
      table.SelectionChanged += self.__change_table_selection_fired
      return table


   # ==========================================================================
   def __build_okbutton(self):
      ''' builds and returns the ok button for this form '''
      
      button = Button()
      button.DialogResult = DialogResult.OK
      button.Location = Point(15, 362)
      button.Size = Size(90, 24)
      button.Text = i18n.get("SeriesFormOK")
      return button
   
   
   # ==========================================================================
   def __build_skipbutton(self):
      ''' builds and return the skip button for this form '''
      
      button = Button()
      button.DialogResult = DialogResult.Ignore
      button.Location = Point(110, 362)
      button.Size = Size(90, 24)
      button.Text = i18n.get("SeriesFormSkip")
      return button


   # ==========================================================================
   def __build_searchbutton(self):
      ''' builds and return the 'search again' button for this form '''
      
      button = Button()
      button.DialogResult = DialogResult.Retry
      button.Location = Point(275, 362) \
         if self.__config.show_covers_b else Point(485, 362) 
      button.Size = Size(115, 24)
      button.Text = i18n.get("SeriesFormAgain")
      return button
   
   # ==========================================================================
   def __build_issuesbutton(self):
      ''' builds and return the 'show issues' button for this form '''
      
      button = Button()
      button.DialogResult = DialogResult.Yes
      button.Location = Point(395, 362) \
         if self.__config.show_covers_b else Point(605, 362) 
      button.Size = Size(115, 24)
      button.Text = i18n.get("SeriesFormIssues")
      return button
   
   
   # ==========================================================================
   def __build_label(self, search_terms_s, num_matches_n):
      ''' 
      Builds and return the text label for this form.
      'search_terms_s' -> user's search string that was used to find series
      'num_matches_n' -> number of series (table rows) the user's search matched
      '''
      
      label = Label()
      label.UseMnemonic = False
      label.Location = Point(10, 20)
      label.Size = Size(480, 40)
      if num_matches_n > 1:
         label.Text = i18n.get("SeriesFormChooseText")\
            .format(search_terms_s, num_matches_n )
      else:
         label.Text = i18n.get("SeriesFormConfirmText").format(search_terms_s)
      return label
   

   # ==========================================================================
   def __build_coverpanel(self, book):
      ''' 
      Builds and returns the cover image PictureBox for this form.
      'book' -> the ComicBook being scraped
      '''
      panel = IssueCoverPanel(self.__config, -9991 \
         if self.__config.force_series_art_b else book.issue_num_s) 
      panel.Location = Point(523, 30)
      # panel size is determined by the panel itself
      
      if self.__config.show_covers_b:
         panel.Show()
      else:
         panel.Hide()
      return panel
   
   # ==========================================================================
   def show_form(self):
      '''
      Displays this form, blocking until the user closes it.  When it is closed,
      it will return a SeriesFormResult describing how it was closed, and any
      SeriesRef that may have been chosen when it was closed. 
      '''
      
      dialogAnswer = self.ShowDialog(self.Owner) # blocks
      
      if dialogAnswer == DialogResult.OK:
         series = self.__series_refs[self.__chosen_index] 
         result = SeriesFormResult( "OK", series )
         alt_choice = self.__coverpanel.get_alt_issue_cover_choice()
         if alt_choice:
            issue_ref, image_ref = alt_choice
            # the user chose a non-default cover image for this issue.
            # we'll store that choice in the global "session data map",
            # in case any other part of the program wants to use it.
            alt_cover_key = sstr(issue_ref.issue_key) + "-altcover"
            self.__config.session_data_map[alt_cover_key] = image_ref
      elif dialogAnswer == DialogResult.Yes:
         series = self.__series_refs[self.__chosen_index] 
         result = SeriesFormResult( "SHOW", series )
      elif dialogAnswer == DialogResult.Cancel: 
         result = SeriesFormResult( "CANCEL")
      elif dialogAnswer == DialogResult.Ignore:
         if self.ModifierKeys == Keys.Control:
            result = SeriesFormResult( "PERMSKIP" )
         else:
            result = SeriesFormResult( "SKIP" )
      elif dialogAnswer == DialogResult.Retry:
         result = SeriesFormResult( "SEARCH" )
      else:
         raise Exception()
      
      return result


   #===========================================================================
   def __form_closed_fired(self, sender, args):
      ''' this method is called whenever this SeriesForm is closed. '''
      
      self.__table.Dispose()
      self.__coverpanel.free()      
      self.Closed -= self.__form_closed_fired


   #===========================================================================         
   def __change_table_selection_fired(self, sender, args):
      ''' this method is called whenever the table's selected row changes. '''
      
      # update __chosen_index (eventually used as this dialog's return value)
      # and then also use it to update the displayed cover image.
      selected_rows = self.__table.SelectedRows
      if selected_rows.Count == 1:
         self.__chosen_index = selected_rows[0].Cells[6].Value
         self.__coverpanel.set_ref(
            self.__series_refs[self.__chosen_index])
      else:
         self.__chosen_index = None
         self.__coverpanel.set_ref(None) 
     
      # don't let the user click 'ok' or 'show issue' if no row is selected!    
      self.__ok_button.Enabled = selected_rows.Count == 1
      self.__issues_button.Enabled = selected_rows.Count == 1
      
               
   #===========================================================================         
   def __key_was_pressed(self, sender, args):
      ''' Called whenever the user presses any key on this form. '''
      
      # highlight the skip button whenever the user presses control key
      if args.KeyCode == Keys.ControlKey and not self.__pressing_controlkey:
         self.__pressing_controlkey = True;
         self.__skip_button.Text = "- " + i18n.get("SeriesFormSkip") + " -"
         
   #===========================================================================         
   def __key_was_released(self, sender, args):
      ''' Called whenever the user releases any key on this form. '''
      
      # unhighlight the skip button bold whenever the user releases control key
      if args.KeyCode == Keys.ControlKey:
         self.__pressing_controlkey = False;
         self.__skip_button.Text = i18n.get("SeriesFormSkip")

   #===========================================================================         
   def __was_deactivated(self, sender, args):
      ''' Called whenever this form gets deactivated, for any reason '''
      
      # unhighlight the skip button bold whenever we deactivate
      if self.__pressing_controlkey:
         self.__pressing_controlkey = False;
         self.__skip_button.Text = i18n.get("SeriesFormSkip")