Search in toolbar

occ <[email protected]> Fri, 24 Feb 2006 15:07:55 +0000
Newsgroups gmane.comp.gnome.apps.pybliographer
Message-ID <[email protected]>
First of all apologies if this has already been done, i haven't had time
to scan for this on the mailing list or test a newer version than 1.2.5 

I've been using pybliographic to manage my thesis bib file for a few
months now, and over time i've noticed that for searching (my most
common interaction with the UI) the number of user interface steps is
somewhat cumbersome. 

A typical search for me (in 1.2.5) consists of: 
 * Selecting find
 * Deselecting the previous search 
 * Entering the search term 
 * Clicking find

In light of this i've done a quick hack to add a search box to the
toolbar (along the lines of firefox etc.) to shortcut the first two
stages mentioned above. The search button sets the filter  (in the same
way as the find dialog box) on all keys. 

It doesn't look particularly pretty, and i'm sure that there are
improvements that people could make but it does speed up searching. 

attached are a screenshot, my modified Document.py and glade files

I will make a patch if this is useful, although the changes are fairly
trivial. 

Owen 

 
---------------------------------------------------------------------
owen cliffe (postgraduate)          Email: [email protected]
Department of Computer Science        Web: http://www.cs.bath.ac.uk/~occ
Univ. Bath, Bath, England BA2 7AY      Tel: (+44) 1225 386183
pybliographic.png (image/png, 31.9 KB) - not displayed
Document.py (text/x-python, 24.6 KB)
# -*- coding: utf-8 -*-
#
# This file is part of pybliographer
# 
# Copyright (C) 1998-2004 Frederic GOBRY
# Email : gobry-RCRJoSF0iyBS/hBiO3TeyNi2O/[email protected]
# 	   
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2 
# of the License, or (at your option) any later version.
#   
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details. 
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# 
# 

''' This module defines a Document class '''

from gnome import ui

import gnome
import gtk
import gtk.glade

from Pyblio.GnomeUI import Index, Entry, Utils, FileSelector, Editor
from Pyblio.GnomeUI import Search, Format
from Pyblio.GnomeUI.Sort import SortDialog
from Pyblio.GnomeUI.Medline import MedlineUI

from Pyblio import Connector, Open, Exceptions, Selection, Sort, Base, Config 
from Pyblio import Search as SearchCls
from Pyblio import version, Fields, Types, Query

import Pyblio.Style.Utils

import os, string, copy, types, sys, traceback, stat

import cPickle

pickle = cPickle
del cPickle

printable = string.lowercase + string.uppercase + string.digits

class Document (Connector.Publisher):
    
    def __init__ (self, database):


        gp = os.path.join (version.pybdir, 'glade', 'pyblio.glade')
        
        self.xml = gtk.glade.XML (gp, 'main', domain = 'pybliographer')
        self.xml.signal_autoconnect (self)

        self.w = self.xml.get_widget ('main')
        self.paned = self.xml.get_widget ('main_pane')
        self.searchbox = self.xml.get_widget ('searchbox')
        self.w_save_btn = self.xml.get_widget ('_w_save_btn')
        self.w_save_mnu = self.xml.get_widget ('_w_save_mnu')
        
        # The Index list
        self.index = Index.Index ()
        self.paned.add1 (self.index.w)
        
        self.index.Subscribe ('new-entry',      self.add_entry)
        self.index.Subscribe ('edit-entry',     self.edit_entry)
        self.index.Subscribe ('delete-entry',   self.delete_entry)
        self.index.Subscribe ('select-entry',   self.update_display)
        self.index.Subscribe ('select-entries', self.freeze_display)
        self.index.Subscribe ('drag-received',  self.drag_received)
        self.index.Subscribe ('drag-moved',     self.drag_moved)
        self.index.Subscribe ('click-on-field', self.sort_by_field)

        self.paned.show_all ()

        # The text area
        self.display = Entry.Entry ()
        self.paned.add2 (self.display.w)

        # Status bar
        self.statusbar = self.xml.get_widget ('statusbar')
        
        # set window size
        ui_width  = Utils.config.get_int ('/apps/pybliographic/ui/width') or -1
        ui_height = Utils.config.get_int ('/apps/pybliographic/ui/height') or -1

        if ui_width != -1 and ui_height != -1:
            self.w.set_default_size (ui_width, ui_height)

        # set paned size
        paned_height = Utils.config.get_int ('/apps/pybliographic/ui/paned') or -1
        self.paned.set_position (paned_height)
        
        self.w.show_all ()
        
        # application variables
        self.data      = database
        self.selection = Selection.Selection ()
        self.search_dg = None
        self.sort_dg   = None
        self.lyx       = None
        self.changed   = 0
        self.directory = None

        self.incremental_start  = None
        self.incremental_search = ''
        
        self.modification_date = None

        # set the default sort method
        default = Utils.config.get_string ('/apps/pybliographic/sort/default')
        if default is not None: default = pickle.loads (default)

        self.sort_view (default)

        self._title_set ()
        return

    def _title_set (self):
        
        if self.data.key is None:
            self.w.set_title (_('Unnamed bibliographic database'))
            return

        name = os.path.basename (self.data.key.url [2])
        
        self.w.set_title (name)
        return
        

    def set_preferences (self, * arg):
        from Pyblio.GnomeUI import Config
        Config.run(self.w)
        return

    def set_fields (self, * arg):
        from Pyblio.GnomeUI import Fields
        Fields.run (self.w)
        return
    
    def forget_changes_cb (self, * arg):
        Config.forget_changes()
        return
    

    def update_history (self, history):
        ''' fill the " Previous Documents " menu with the specified list of documents '''

        sub = self.xml.get_widget ('previous_documents')
        factory = gtk.ItemFactory (gtk.Menu, '<main>', None)

        # This list contains the info needed to create the "Previous Documents" menu.
        menuinfo = []

        # This list contains the names of the menu entries that can be
        # fetched back by factory.get_widget calls. The two lists
        # differ as the factory.create_items () function interprets
        # underscores to mark shortcuts, whereas the get_widget call
        # does not escape the underscores...
        names = []
        
        for item in history:
            # Display name in the menu
            filename = string.replace (item [0], '/', '\/')
            quoted   = string.replace (filename, '_', '__')

            menuinfo.append (('/' + quoted, None,
                              self._history_open_cb, 0, None))
            names.append ('/' + filename)

        factory.create_items (menuinfo)

        # Bind the actual file info to each menu entry
        i = 0
        for item in names:
            w = factory.get_widget (item)
            w.set_data ('file', history [i])
            i = i + 1
            
        sub.set_submenu (factory.get_widget ('<main>'))
        return


    def _history_open_cb (self, id, w):

        file, type = w.get_data ('file')
        
        if not self.confirm (): return

        self.open_document (file, type)
        return
    
    
    def redisplay_index (self, changed = -1):
        ''' redisplays the index. If changed is specified, set the
        self.changed status to the given value '''
        
        if changed != -1:
            self.changed = changed

        self.index.display (self.selection.iterator (self.data.iterator ()))
        
        self.update_status ()
        return


    def format_query (self, style, format, output):
        try:
            file = open (output, 'w')
        except IOError, err:
            self.w.error (_("can't open file `%s' for writing:\n%s")
                          % (output, str (err)))
            return
        
        entries = map (lambda x: x.key, self.index.selection ())
        
        if not entries:
            iter    = self.selection.iterator (self.data.iterator ())
            entries = []
            
            e = iter.first ()
            while e:
                entries.append (e.key)
                e = iter.next ()

        url = Fields.URL (style)

        try:
            Pyblio.Style.Utils.generate (url, format, self.data, entries, file)
        except RuntimeError, err:
            print err
            self.w.error (_("Error while parsing `%s':\n%s") % (style, err))
        return


    def format_entries (self, * arg):
        format_dg = Format.FormatDialog (self.w)
        format_dg.Subscribe ('format-query', self.format_query)
        return

    
    def update_status (self, status = -1):
        ''' redisplay status bar according to the current status '''

        if status != -1: self.changed = status
        
        if self.data.key is None:
            text = _("New database")
        else:
            text = str (self.data.key)

        li = len (self.index)
        ld = len (self.data)
        
        if li == ld:
            if   ld == 0: num = _("[no entry]")
            elif ld == 1: num = _("[1 entry]")
            else:         num = _("[%d entries]")    %  ld
        else:
            if   ld == 0: num = _("[no entry]")
            elif ld == 1: num = _("[%d/1 entry]")    % li
            else:         num = _("[%d/%d entries]") % (li, ld)

        text = text + ' ' + num
        
        if self.changed:
            text = text + ' ' + _("[modified]")

        self.w_save_btn.set_sensitive (self.changed)
        self.w_save_mnu.set_sensitive (self.changed)

        self.statusbar.set_default (text)
        return

    
    def confirm (self):
        ''' eventually ask for modification cancellation '''
        
        if self.changed:
            return Utils.Callback (_("The database has been modified.\nDiscard changes ?"),
                                   self.w).answer ()
        
        return 1

        
    def new_document (self, * arg):
        ''' callback corresponding to the "New Document" button '''
        
        self.issue ('new-document', self)
        return


    def query_database (self, * arg):
        ''' callback corresponding to the "Medline Query..." button '''

        if not self.confirm (): return

        data = MedlineUI (self.w).run ()
        if data is None: return
        
        url = apply (Query.medline_query, data)

        if url is None:
            # no result.
            self.w.error (_("Your query returned no result"))
            return
        
        self.open_document (url, 'medline', no_name = True)
        return


    def merge_database (self, * arg):
        ''' add all the entries of another database to the current one '''
        # get a new file name
        (url, how) = FileSelector.URLFileSelection (_("Merge file"),
                                                    url = True, has_auto = True).run ()

        if url is None: return

        try:
            iterator = Open.bibiter (url, how = how)
            
        except (Exceptions.ParserError,
                Exceptions.FormatError,
                Exceptions.FileError), error:
            
            Utils.error_dialog (_("Open error"), error,
                                parent = self.w)
            return

        # loop over the entries
        errors = []
        try:
            entry = iterator.first ()
        except Exceptions.ParserError, msg:
            errors = errors + msg.errors
        
        while entry:
            self.data.add (entry)
            while 1:
                try:
                    entry = iterator.next ()
                    break
                except Exceptions.ParserError, msg:
                    errors = errors + list (msg.errors)
                    continue

        self.redisplay_index (1)

        if errors:
            Utils.error_dialog (_("Merge status"), string.join (errors, '\n'),
                                parent = self.w)
        return

        
    def ui_open_document (self, * arg):
        ''' callback corresponding to "Open" '''
        
        if not self.confirm (): return

        # get a new file name
        (url, how) = FileSelector.URLFileSelection (_("Open file")).run ()

        if url is None: return
        self.open_document (url, how)
        return

    
    def open_document (self, url, how = None, no_name = False):

        Utils.set_cursor (self.w, 'clock')
        
        try:
            data = Open.bibopen (url, how = how)
            
        except (Exceptions.ParserError,
                Exceptions.FormatError,
                Exceptions.FileError), error:
            
            Utils.set_cursor (self.w, 'normal')
            Utils.error_dialog (_("Open error"), error,
                                parent = self.w)
            return

        Utils.set_cursor (self.w, 'normal')

        if no_name: data.key = None
        
        self.data    = data
        self.redisplay_index (0)
        
        self._title_set ()

        # eventually warn interested objects
        self.issue ('open-document', self)
        return

    
    def save_document (self, * arg):
        if self.data.key is None:
            self.save_document_as ()
            return

        file = self.data.key.url [2]
        
        if self.modification_date:
            mod_date = os.stat (file) [stat.ST_MTIME]
            
            if mod_date > self.modification_date:
                if not Utils.Callback (_("The database has been externally modified.\nOverwrite changes ?"),
                                       self.w).answer ():
                    return
        
        Utils.set_cursor (self.w, 'clock')
        try:
            try:
                self.data.update (self.selection.sort)
            except (OSError, IOError), error:
                Utils.set_cursor (self.w, 'normal')
                self.w.error (_("Unable to save `%s':\n%s") % (str (self.data.key),
                                                               str (error)))
                return
        except:
            etype, value, tb = sys.exc_info ()
            traceback.print_exception (etype, value, tb)
            
            Utils.set_cursor (self.w, 'normal')
            self.w.error (_("An internal error occured during saving\nTry to Save As..."))
            return

        Utils.set_cursor (self.w, 'normal')

        # get the current modification date
        self.modification_date = os.stat (file) [stat.ST_MTIME]
        
        self.update_status (0)
        return
    
    
    def save_document_as (self, * arg):
        # get a new file name
        (url, how) = FileSelector.URLFileSelection (_("Save As..."),
                                                    url = False, has_auto = False, is_save = True).run ()
        
        if url is None: return

        if os.path.exists (url):
            if not Utils.Callback (_("The file `%s' already exists.\nOverwrite it ?")
                                   % url, parent = self.w).answer ():
                return

        try:
            file = open (url, 'w')
        except IOError, error:
            self.w.error (_("During opening:\n%s") % error [1])
            return

        Utils.set_cursor (self.w, 'clock')

        iterator = Selection.Selection (sort = self.selection.sort)
        Open.bibwrite (iterator.iterator (self.data.iterator ()),
                       out = file, how = how)
        file.close ()
        
        try:
            self.data = Open.bibopen (url, how = how)
                
        except (Exceptions.ParserError,
                Exceptions.FormatError,
                Exceptions.FileError), error:
                    
            Utils.set_cursor (self.w, 'normal')
            Utils.error_dialog (_("Reopen error"), error,
                                parent = self.w)
            return
            
        self.redisplay_index ()
        self._title_set ()

        self.issue ('open-document', self)
            
        Utils.set_cursor (self.w, 'normal')

        self.update_status (0)
        return

    
    def close_document (self, * arg):
        self.issue ('close-document', self)
        return 1

    def close_or_exit (self, * arg):
        self.issue ('close-document', self, True)
        return 1


    def close_document_request (self):
        return self.confirm ()

    
    def exit_application (self, * arg):
        self.issue ('exit-application', self)
        return


    def drag_moved (self, entries):
        if not entries: return
        
        for e in entries:
            del self.data [e.key]

        self.redisplay_index (1)
        return

    
    def drag_received (self, entries):
        for entry in entries:
            
            if self.data.would_have_key (entry.key):
                if not Utils.Callback (_("An entry called `%s' already exists.\nRename and add it anyway ?")
                                       % entry.key.key, parent = self.w).answer ():
                    continue
                
            self.changed = 1
            self.data.add (entry)

        self.redisplay_index ()
        self.index.set_scroll (entries [-1])
        return

                
    def cut_entry (self, * arg):
        entries = self.index.selection ()
        if not entries: return
        
        self.index.selection_copy (entries)
        for entry in entries:
            del self.data [entry.key]
            
        self.redisplay_index (1)
        pass

    
    def copy_entry (self, * arg):
        self.index.selection_copy (self.index.selection ())
        return

    
    def paste_entry (self, * arg):
        self.index.selection_paste ()
        return

    
    def clear_entries (self, * arg):
        if len (self.data) == 0: return

        if not Utils.Callback (_("Really remove all the entries ?"),
                               parent = self.w).answer ():
            return

        keys = self.data.keys ()
        for key in keys:
            del self.data [key]

        self.redisplay_index (1)
        return
    
    
    def select_all_entries (self, * arg):
        self.index.select_all ()
        return
    
    
    def add_entry (self, * arg):
        entry = self.data.new_entry (Config.get ('base/defaulttype').data)
        
        edit = Editor.Editor (self.data, entry, self.w, _("Create new entry"))
        edit.Subscribe ('commit-edition', self.commit_edition)
        return

    
    def edit_entry (self, entries):
        if not (type (entries) is types.ListType):
            entries = self.index.selection ()
        
        l = len (entries)

        if l == 0: return
        
        if l > 5:
            if not Utils.Callback (_("Really edit %d entries ?") % l):
                return

        for entry in entries:
            edit = Editor.Editor (self.data, entry, self.w)
            edit.Subscribe ('commit-edition', self.commit_edition)

        return


    def commit_edition (self, old, new):
        ''' updates the database and the display '''

        if old.key != new.key:
            if self.data.has_key (old.key):
                del self.data [old.key]

        if new.key:
            self.data [new.key] = new
        else:
            self.data.add (new)

        self.freeze_display (None)

        self.redisplay_index (1)
        self.index.select_item (new)
        return
    
    
    def delete_entry (self, * arg):
        ''' removes the selected list of items after confirmation '''
        entries = self.index.selection ()
        l = len (entries)
        if l == 0: return

        offset = self.index.get_item_position (entries [-1])

        if l > 1:
            question = _("Remove all the %d entries ?") % len (entries)
        else:
            question = _("Remove entry `%s' ?") % entries [0].key.key
            
        if not Utils.Callback (question,
                               parent = self.w).answer ():
            return

        for entry in entries:
            del self.data [entry.key]
            
        self.redisplay_index (1)
        self.index.select_item (offset)
        return
    
    
    def find_entries (self, * arg):
        if self.search_dg is None:
            self.search_dg = Search.SearchDialog (self.w)
            self.search_dg.Subscribe ('search-data', self.limit_view)
        else:
            self.search_dg.show ()
        return

    def quick_search (self, * arg):
        self.limit_view(SearchCls.AnyTester (self.searchbox.get_text().encode ('latin-1')));
        return

    def limit_view (self, search):
        print search
        self.selection.search = search
        self.redisplay_index ()
        return

    
    def sort_entries (self, * arg):
        sort_dg = SortDialog (self.selection.sort, self.w)
        sort_dg.Subscribe ('sort-data', self.sort_view)
        return


    def sort_view (self, sort):
        self.selection.sort = sort
        self.redisplay_index ()
        return
    

    def sort_by_field (self, field):
        if field == '-key-':
            mode = Sort.KeySort ()

        elif field == '-type-':
            mode = Sort.TypeSort ()
        else:
            mode = Sort.FieldSort (field)

        # Check if we are toggling or changing
        cur = self.selection.sort
        
        if cur and len (cur.fields) == 1:
            cur = cur.fields [0]

            # We are still filtering according to the same field,
            # simply toggle the direction
            if cur == mode:
                mode.ascend = - cur.ascend
            
        self.selection.sort = Sort.Sort ([mode])
        self.redisplay_index ()
        return


    def lyx_cite (self, * arg):

        import locale

        try:
            enc = locale.getpreferredencoding ()

        except AttributeError:
            enc = locale.getdefaultlocale()[1]

        
        entries = self.index.selection ()
        if not entries: return
        
        if self.lyx is None:
            from Pyblio import LyX

            try:
                self.lyx = LyX.LyXClient ()
            except IOError, msg:
                msg = msg [1].decode (enc)
                self.w.error (_("Can't connect to LyX:\n%s") % msg)
                return

        keys = string.join (map (lambda x: x.key.key, entries), ', ')
        try:
            self.lyx ('citation-insert', keys)
        except IOError, msg:
            msg = msg [1].decode (enc)
            self.w.error (_("Can't connect to LyX:\n%s") % msg)
        return
    

    def update_display (self, entry):
        self.display.display (entry)
        return

    
    def freeze_display (self, entry):
        self.display.clear ()
        return


    def key_pressed (self, app, event):
        # filter out special keys
        if (event.string < 'a' or event.string > 'z') and \
           (event.string < '0' or event.string > '9'): return 1

        if self.selection.sort is None:
            app.flash ("Select a column to search in first.")
            return 1
        
        if event.string in printable:
            # the user searches the first entry in its ordering that starts with this letter
            if self.incremental_search == '':
                self.incremental_search = event.string
                self.incremental_start  = event.time
            else:
                if event.time - self.incremental_start > 1000:
                    self.incremental_search = event.string
                else:
                    # two keys in a same shot: we search for the composition of the words
                    self.incremental_search = self.incremental_search + event.string
                
                self.incremental_start  = event.time

            # search first occurence
            if self.index.go_to_first (self.incremental_search,
                                       self.selection.sort.fields [0]):
                app.flash ("Searching for '%s...'" % self.incremental_search)
            else:
                app.flash ("Cannot find '%s...'" % self.incremental_search)
                
        return 1


    def update_configuration (self):
        ''' save current informations about the program '''
        
        # Save the graphical aspect of the interface
        # 1.- Window size
        alloc = self.w.get_allocation ()
        Utils.config.set_int ('/apps/pybliographic/ui/width',  alloc [2])
        Utils.config.set_int ('/apps/pybliographic/ui/height', alloc [3])

        # 2.- Proportion between list and text
        height = self.paned.get_position ()
        Utils.config.set_int ('/apps/pybliographic/ui/paned', height)

        # updates the index's config
        self.index.update_configuration ()

        return

    def on_documentation (self, *args):
        import gobject

        try:
            gnome.help_display ('pybliographer', 'getting-started')
            
        except gobject.GError, msg:
            self.w.error (_("Can't display documentation:\n%s") % msg)
            
        return
    
    def about (self, *arg):
        
        about = ui.About ('Pybliographic',
                          version.version,
                          _("This program is copyrighted under the GNU GPL"),
                          _("Gnome interface to the Pybliographer system."),
                          ['Hervé Dréau',
                           'Frédéric Gobry',
                           'Travis Oliphant',
                           'Darrell Rudmann',
                           'Peter Schulte-Stracke',
                           'John Vu'],
                          ['Yuri Bongiorno',
                           'Frédéric Gobry',
                           'Zoltán Kóta'],
                          _('Gnome Translation Team'))

        about.set_transient_for (self.w)
        
        link = ui.HRef ('http://www.pybliographer.org/',
                        _("Pybliographer Home Page"))
        link.show ()
        about.vbox.pack_start (link)
        about.show()
        return
pyblio.glade (application/x-glade, 25.1 KB) - not displayed