diff -Nru catfish-1.0.2/build.py catfish-1.2.2/build.py
--- catfish-1.0.2/build.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/build.py 2014-09-21 21:34:50.000000000 +0000
@@ -23,7 +23,7 @@
print('No arguments supplied.')
sys.exit(1)
-#lint:disable
+# lint:disable
if sys.argv[1] == 'check':
print('Checking module dependencies...')
try:
@@ -42,12 +42,12 @@
from xml.sax.saxutils import escape, unescape
from gi.repository import GObject, Gtk, Gdk, GdkPixbuf, Pango
import zeitgeist # optional
-#lint:enable
+# lint:enable
except ImportError as msg:
print((str(msg)))
module = str(msg).split()[-1]
- if not module in ('zeitgeist'):
+ if module != 'zeitgeist':
print(('...Error: The required module %s is missing.' % module))
sys.exit(1)
else:
diff -Nru catfish-1.0.2/catfish/CatfishSearchEngine.py catfish-1.2.2/catfish/CatfishSearchEngine.py
--- catfish-1.0.2/catfish/CatfishSearchEngine.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish/CatfishSearchEngine.py 2014-09-21 21:34:50.000000000 +0000
@@ -19,6 +19,7 @@
import logging
logger = logging.getLogger('catfish_search')
+import io
import os
import signal
import subprocess
@@ -108,7 +109,7 @@
self.keywords = keywords
wildcard_chunks = []
- for key in self.keywords.split(' '):
+ for key in self.keywords.split():
if '*' in key:
wildcard_chunks.append(key.split('*'))
@@ -226,7 +227,8 @@
True if still running."""
self.running = True
if isinstance(keywords, str):
- keywords = keywords.split(', ')
+ keywords = keywords.replace(',', ' ')
+ keywords = keywords.split()
for root, dirs, files in os.walk(path):
if not self.running:
break
@@ -275,7 +277,7 @@
# Split the keywords into a list if they are not already.
if isinstance(keywords, str):
keywords = keywords.replace(',', ' ')
- keywords = keywords.split(' ')
+ keywords = keywords.split()
for keyword in keywords:
if keyword.lower() not in find_keywords_backup:
@@ -290,7 +292,7 @@
if self.force_stop:
break
- #If the filetype is known to not be text, ignore it and move on
+ # If the filetype is known to not be text, move along.
mime = guess_type(filename)[0]
if not mime or 'text' in mime:
try:
@@ -399,7 +401,7 @@
"""Initialize the external method class."""
CatfishSearchMethod.__init__(self, method_name)
self.pid = -1
- self.command = None
+ self.command = []
self.process = None
def assemble_query(self, keywords, path):
@@ -417,17 +419,21 @@
if regex:
command = self.assemble_query(keywords, path)
if not command:
- command = self.command.replace('%keywords', keywords.lower())
- if '%path' in command:
- command = command.replace('%path', path)
+ command = [item.replace('%keywords', keywords.lower())
+ for item in self.command]
+ command = [item.replace('%path', path) for item in command]
self.process = subprocess.Popen(command, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE, shell=True)
+ stderr=subprocess.PIPE, shell=False)
self.pid = self.process.pid
return self.process_output(self.process.stdout)
def process_output(self, output):
"""Return the output text."""
- return output
+ if isinstance(output, io.BufferedReader):
+ return map(lambda s: s.decode(encoding='UTF8').strip(),
+ output.readlines())
+ else:
+ return output
def status(self):
"""Return the current search status."""
@@ -457,8 +463,9 @@
def __init__(self):
"""Initialize the Locate SearchMethod."""
CatfishSearchMethodExternal.__init__(self, "locate")
- self.command = "locate -i %path*%keywords* --existing"
+ self.command = ["locate", "-i", "%path*%keywords*", "--existing"]
def assemble_query(self, keywords, path):
"""Assemble the search query."""
- return "locate --regex -i \"%s\"" % string_regex(keywords, path)
+ return ["locate", "--regex", "-i", "{}".format(string_regex(keywords,
+ path))]
diff -Nru catfish-1.0.2/catfish/CatfishWindow.py catfish-1.2.2/catfish/CatfishWindow.py
--- catfish-1.0.2/catfish/CatfishWindow.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish/CatfishWindow.py 2014-09-21 21:34:50.000000000 +0000
@@ -34,29 +34,30 @@
import logging
logger = logging.getLogger('catfish')
-from catfish_lib import Window, CatfishSettings, SudoDialog
+from catfish_lib import Window, CatfishSettings, SudoDialog, helpers
from catfish.AboutCatfishDialog import AboutCatfishDialog
from catfish.CatfishSearchEngine import *
import pexpect
-import sys
-pyversion = float(str(sys.version_info[0]) + '.' + str(sys.version_info[1]))
-
# Initialize Gtk, GObject, and mimetypes
-if GObject.pygobject_version < (3, 9, 1):
+if not helpers.check_gobject_version(3, 9, 1):
GObject.threads_init()
GLib.threads_init()
mimetypes.init()
-def application_in_PATH(application_name):
- """Return True if the application name is found in PATH."""
+def get_application_path(application_name):
for path in os.getenv('PATH').split(':'):
if os.path.isdir(path):
if application_name in os.listdir(path):
- return True
- return False
+ return os.path.join(path, application_name)
+ return None
+
+
+def application_in_PATH(application_name):
+ """Return True if the application name is found in PATH."""
+ return get_application_path(application_name) is not None
def is_file_hidden(folder, filename):
@@ -203,16 +204,39 @@
self.file_menu = builder.get_object("file_menu")
self.file_menu_save = builder.get_object("menu_save")
self.file_menu_delete = builder.get_object("menu_delete")
+ self.treeview_click_on = False
- # -- Update Search Index dialog -- #
- self.update_index_dialog = builder.get_object("update_index_dialog")
- self.update_index_spinner = builder.get_object("update_index_spinner")
- self.update_index_updating = builder.get_object(
- "update_index_updating")
- self.update_index_done = builder.get_object("update_index_done")
- self.update_index_close = builder.get_object("update_index_close")
- self.update_index_unlock = builder.get_object("update_index_unlock")
- self.update_index_active = False
+ # -- Update Search Index Dialog -- #
+ menuitem = builder.get_object('menu_update_index')
+ if SudoDialog.check_dependencies(['locate', 'updatedb']):
+ self.update_index_dialog = \
+ builder.get_object("update_index_dialog")
+ self.update_index_database = builder.get_object("database_label")
+ self.update_index_modified = builder.get_object("updated_label")
+ self.update_index_infobar = builder.get_object("update_status")
+ self.update_index_icon = builder.get_object("update_status_icon")
+ self.update_index_label = builder.get_object("update_status_label")
+ self.update_index_close = builder.get_object("update_index_close")
+ self.update_index_unlock = \
+ builder.get_object("update_index_unlock")
+ self.update_index_active = False
+
+ now = datetime.datetime.now()
+ self.today = datetime.datetime(now.year, now.month, now.day)
+ locate, locate_path, locate_date = self.check_locate()
+
+ self.update_index_database.set_label("%s" % locate_path)
+ if os.path.isfile(locate_path):
+ modified = locate_date.strftime("%x %X")
+ else:
+ modified = _("Never")
+ self.update_index_modified.set_label("%s" % modified)
+
+ if locate_date < self.today - datetime.timedelta(days=7):
+ infobar = builder.get_object("locate_infobar")
+ infobar.show()
+ else:
+ menuitem.hide()
self.format_mimetype_box = builder.get_object("format_mimetype_box")
self.extensions_entry = builder.get_object("extensions")
@@ -234,6 +258,12 @@
builder)
self.settings = CatfishSettings.CatfishSettings()
+ self.refresh_search_entry()
+
+ def on_update_infobar_response(self, widget, response_id):
+ if response_id == Gtk.ResponseType.OK:
+ self.on_menu_update_index_activate(widget)
+ widget.hide()
def on_floating_bar_enter_notify(self, widget, event):
"""Move the floating statusbar when hovered."""
@@ -297,12 +327,30 @@
"""Parse commandline arguments into Catfish runtime settings."""
self.options = options
- if not application_in_PATH(self.options.fileman):
- self.options.fileman = None
- if not application_in_PATH(self.options.open_wrapper):
- self.options.open_wrapper = None
+ # Set the selected folder path. Allow legacy --path option.
+ path = None
+
+ # New format, first argument
+ if self.options.path is None:
+ if len(args) > 0:
+ if os.path.isdir(os.path.realpath(args[0])):
+ path = args.pop(0)
+
+ # Old format, --path
+ else:
+ if os.path.isdir(os.path.realpath(self.options.path)):
+ path = self.options.path
+
+ # Make sure there is a valid path.
+ if path is None:
+ path = os.path.expanduser("~")
+ if os.path.isdir(os.path.realpath(path)):
+ self.options.path = path
+ else:
+ path = "/"
+ else:
+ self.options.path = path
- # Set the selected folder path.
self.folderchooser.set_filename(self.options.path)
# Set non-flags as search keywords.
@@ -340,6 +388,9 @@
self.setup_small_view()
self.list_toggle.set_active(True)
+ if self.options.start:
+ self.on_search_entry_activate(self.search_entry)
+
def preview_cell_data_func(self, col, renderer, model, treeiter, data):
"""Cell Renderer Function for the preview."""
icon_name = model[treeiter][0]
@@ -364,13 +415,13 @@
def thumbnail_cell_data_func(self, col, renderer, model, treeiter, data):
"""Cell Renderer Function to Thumbnails View."""
icon, name, size, path, modified, mime, hidden, exact = \
- model[treeiter][:]
+ model[treeiter][:]
name = escape(name)
size = self.format_size(size)
path = escape(path)
modified = self.get_date_string(modified)
displayed = '%s %s%s%s%s%s' % (name, size, os.linesep, path,
- os.linesep, modified)
+ os.linesep, modified)
renderer.set_property('markup', displayed)
return
@@ -385,7 +436,7 @@
icon_lookup_flags)
color = context.get_color(state)
icon = icon_info.load_symbolic(color, color, color, color)[0]
- except AttributeError:
+ except (AttributeError, GLib.GError):
icon_lookup_flags = Gtk.IconLookupFlags.FORCE_SVG | \
Gtk.IconLookupFlags.USE_BUILTIN | \
Gtk.IconLookupFlags.GENERIC_FALLBACK
@@ -393,38 +444,102 @@
icon_name, size, icon_lookup_flags)
return icon
+ def check_locate(self):
+ """Evaluate which locate binary is in use, its path, and modification
+ date. Return these values in a tuple."""
+ path = get_application_path('locate')
+ if path is None:
+ return None
+ path = os.path.realpath(path)
+ locate = os.path.basename(path)
+ db = os.path.join('/var/lib', locate, locate + '.db')
+ if os.path.isfile(db):
+ modified = os.path.getmtime(db)
+ else:
+ modified = 0
+ item_date = datetime.datetime.fromtimestamp(modified)
+ return (locate, db, item_date)
+
# -- Update Search Index dialog -- #
def on_update_index_dialog_close(self, widget=None, event=None,
user_data=None):
"""Close the Update Search Index dialog, resetting to default."""
if not self.update_index_active:
self.update_index_dialog.hide()
- self.update_index_close.set_label(Gtk.STOCK_CANCEL)
+
+ # Restore Unlock button
self.update_index_unlock.show()
- self.update_index_updating.set_sensitive(True)
- self.update_index_updating.hide()
- self.update_index_done.hide()
+ self.update_index_unlock.set_can_default(True)
+ self.update_index_unlock.set_receives_default(True)
+ self.update_index_unlock.grab_focus()
+ self.update_index_unlock.grab_default()
+
+ # Restore Cancel button
+ self.update_index_close.set_label(Gtk.STOCK_CANCEL)
+ self.update_index_close.set_can_default(False)
+ self.update_index_close.set_receives_default(False)
+
+ self.update_index_infobar.hide()
+
return True
+ def show_update_status_infobar(self, status_code):
+ """Display the update status infobar based on the status code."""
+ # Error
+ if status_code in [1, 3]:
+ icon = "dialog-error"
+ msg_type = Gtk.MessageType.WARNING
+ if status_code == 1:
+ status = _('An error occurred while updating the database.')
+ elif status_code == 3:
+ status = _("Authentication failed.")
+
+ # Warning
+ elif status_code == 2:
+ icon = "dialog-warning"
+ msg_type = Gtk.MessageType.WARNING
+ status = _("Authentication cancelled.")
+
+ # Info
+ else:
+ icon = "dialog-information"
+ msg_type = Gtk.MessageType.INFO
+ status = _('Search database updated successfully.')
+
+ self.update_index_infobar.set_message_type(msg_type)
+ self.update_index_icon.set_from_icon_name(icon, Gtk.IconSize.BUTTON)
+ self.update_index_label.set_label(status)
+
+ self.update_index_infobar.show()
+
def on_update_index_unlock_clicked(self, widget):
"""Unlock admin rights and perform 'updatedb' query."""
self.update_index_active = True
# Get the password for sudo
- sudo_dialog = SudoDialog.SudoDialog(
- icon='catfish', name=_("Catfish File Search"), retries=3)
- sudo_dialog.run()
+ sudo_dialog = SudoDialog.SudoDialog(parent=self.update_index_dialog,
+ icon='catfish',
+ name=_("Catfish File Search"),
+ retries=3)
+ response = sudo_dialog.run()
sudo_dialog.hide()
password = sudo_dialog.get_password()
sudo_dialog.destroy()
- if not password:
+ if response in [Gtk.ResponseType.NONE, Gtk.ResponseType.CANCEL]:
self.update_index_active = False
- self.update_index_done.set_label(_("Authentication failed."))
- self.update_index_done.show()
+ self.show_update_status_infobar(2)
return False
- self.update_index_done.hide()
+ elif response == Gtk.ResponseType.REJECT:
+ self.update_index_active = False
+ self.show_update_status_infobar(3)
+ return False
+
+ if not password:
+ self.update_index_active = False
+ self.show_update_status_infobar(2)
+ return False
# Subprocess to check if query has completed yet, runs at end of func.
def updatedb_subprocess():
@@ -436,36 +551,32 @@
done = False
if done:
self.update_index_active = False
+ modified = self.check_locate()[2].strftime("%x %X")
+ self.update_index_modified.set_label("%s" % modified)
+
+ # Hide the Unlock button
+ self.update_index_unlock.set_sensitive(True)
+ self.update_index_unlock.set_receives_default(False)
+ self.update_index_unlock.hide()
+
+ # Update the Cancel button to Close, make it default
self.update_index_close.set_label(Gtk.STOCK_CLOSE)
- self.update_index_updating.set_sensitive(False)
- self.update_index_spinner.hide()
self.update_index_close.set_sensitive(True)
- self.update_index_unlock.hide()
- self.update_index_unlock.set_sensitive(True)
+ self.update_index_close.set_can_default(True)
+ self.update_index_close.set_receives_default(True)
+ self.update_index_close.grab_focus()
+ self.update_index_close.grab_default()
+
return_code = self.updatedb_process.exitstatus
- if return_code == 0:
- status = _('Locate database updated successfully.')
- elif return_code == 1:
- status = _('An error occurred while updating locatedb.')
- elif return_code == 2:
- status = _("User aborted authentication.")
- elif return_code == 3:
- status = _("Authentication failed.")
- else:
- status = _('Locate database updated successfully.')
- self.update_index_done.set_label(status)
- self.update_index_done.show()
+ self.show_update_status_infobar(return_code)
return not done
# Set the dialog status to running.
- self.update_index_spinner.show()
- self.update_index_updating.show()
+ self.update_index_modified.set_label("%s" % _("Updating..."))
self.update_index_close.set_sensitive(False)
self.update_index_unlock.set_sensitive(False)
- self.updatedb_process = pexpect.spawn('sudo updatedb',
- env={"LANG": "C"})
- self.updatedb_process.timeout = 1
+ self.updatedb_process = SudoDialog.env_spawn('sudo updatedb', 1)
try:
# Check for password prompt or program exit.
self.updatedb_process.expect(".*ssword.*")
@@ -480,6 +591,45 @@
GLib.timeout_add(1000, updatedb_subprocess)
# -- Search Entry -- #
+ def refresh_search_entry(self):
+ """Update the appearance of the search entry based on the application's
+ current state."""
+ # Default Appearance, used for blank entry
+ query = None
+ icon_name = "edit-find-symbolic"
+ sensitive = True
+ button_tooltip_text = None
+ entry_tooltip_text = _("Enter search terms and press Enter to begin.")
+
+ # Search running
+ if self.search_in_progress:
+ icon_name = "process-stop"
+ button_tooltip_text = _('Stop Search')
+ entry_tooltip_text = _("Search is in progress...\nPress the "
+ "cancel button or the Escape key to stop.")
+
+ # Search not running
+ else:
+ entry_text = self.search_entry.get_text()
+ # Search not running, value in terms
+ if len(entry_text) > 0:
+ button_tooltip_text = _('Begin Search')
+ query = entry_text
+ else:
+ sensitive = False
+
+ self.search_entry.set_icon_from_icon_name(
+ Gtk.EntryIconPosition.SECONDARY, icon_name)
+ self.search_entry.set_icon_tooltip_text(
+ Gtk.EntryIconPosition.SECONDARY, button_tooltip_text)
+ self.search_entry.set_tooltip_text(entry_tooltip_text)
+ self.search_entry.set_icon_activatable(
+ Gtk.EntryIconPosition.SECONDARY, sensitive)
+ self.search_entry.set_icon_sensitive(
+ Gtk.EntryIconPosition.SECONDARY, sensitive)
+
+ return query
+
def on_search_entry_activate(self, widget):
"""If the search entry is not empty, perform the query."""
if len(widget.get_text()) > 0:
@@ -495,27 +645,19 @@
GLib.idle_add(next, task)
def on_search_entry_icon_press(self, widget, event, user_data):
- """If search in progress, stop the search, otherwise, clear the search
- entry field."""
+ """If search in progress, stop the search, otherwise, start."""
if not self.search_in_progress:
- widget.set_text("")
+ self.on_search_entry_activate(self.search_entry)
else:
self.stop_search = True
self.search_engine.stop()
def on_search_entry_changed(self, widget):
"""Update the search entry icon and run suggestions."""
- text = widget.get_text()
+ text = self.refresh_search_entry()
- if not self.search_in_progress:
- if len(text) == 0:
- icon = Gtk.STOCK_FIND
- msg = _('Enter search terms and press ENTER')
- else:
- icon = Gtk.STOCK_CLEAR
- msg = _('Clear search terms')
- widget.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, icon)
- widget.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, msg)
+ if text is None:
+ return
task = self.get_suggestions(text)
GLib.idle_add(next, task)
@@ -536,8 +678,10 @@
if len(keywords) != 0 and keywords[0] == '.':
show_hidden = True
- model = self.search_entry.get_completion().get_model()
- model.clear()
+ completion = self.search_entry.get_completion()
+ if completion is not None:
+ model = completion.get_model()
+ model.clear()
results = []
for filename in self.suggestions_engine.run(keywords, folder, 10):
@@ -833,33 +977,14 @@
def open_file(self, filename):
"""Open the specified filename in its default application."""
logger.debug("Opening %s" % filename)
- command = None
- if os.path.isdir(filename):
- if self.options.fileman:
- command = [self.options.fileman, filename]
- else:
- # Translators: This message will only appear when trying to
- # open the default file manager but it cannot be found.
- msg = _("Catfish could not find the default file manager.")
- else:
- if self.options.open_wrapper:
- command = [self.options.open_wrapper, filename]
- else:
- # Translators: This message will only appear when the default
- # application for opening a file cannot be found.
- msg = _("Catfish could not find the default open wrapper.")
- if command:
- try:
- subprocess.Popen(command, shell=False)
- return
- except Exception as msg:
- logger.debug('Exception encountered while opening %s.' +
- '\n Exception: %s' +
- '\n The wrapper was %s.' +
- '\n The filemanager was %s.',
- filename, msg,
- str(self.options.open_wrapper),
- str(self.options.fileman))
+ command = ['xdg-open', filename]
+ try:
+ subprocess.Popen(command, shell=False)
+ return
+ except Exception as msg:
+ logger.debug('Exception encountered while opening %s.' +
+ '\n Exception: %s' +
+ filename, msg)
self.get_error_dialog(_('\"%s\" could not be opened.') %
os.path.basename(filename), str(msg))
@@ -943,8 +1068,8 @@
basename = os.path.basename(filename)
dialog = Gtk.FileChooserDialog(title=_('Save "%s" as…') % basename,
- transient_for=self,
- action=Gtk.FileChooserAction.SAVE)
+ transient_for=self,
+ action=Gtk.FileChooserAction.SAVE)
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)
dialog.set_default_response(Gtk.ResponseType.REJECT)
@@ -963,11 +1088,11 @@
escape(secondary))
dialog = Gtk.MessageDialog(transient_for=self,
- modal=True,
- destroy_with_parent=True,
- message_type=Gtk.MessageType.ERROR,
- buttons=Gtk.ButtonsType.OK,
- text="")
+ modal=True,
+ destroy_with_parent=True,
+ message_type=Gtk.MessageType.ERROR,
+ buttons=Gtk.ButtonsType.OK,
+ text="")
dialog.set_markup(dialog_text)
dialog.set_default_response(Gtk.ResponseType.OK)
@@ -988,11 +1113,11 @@
dialog_text = "%s\n\n%s" % (primary, secondary)
dialog = Gtk.MessageDialog(transient_for=self,
- modal=True,
- destroy_with_parent=True,
- message_type=Gtk.MessageType.QUESTION,
- buttons=Gtk.ButtonsType.NONE,
- text="")
+ modal=True,
+ destroy_with_parent=True,
+ message_type=Gtk.MessageType.QUESTION,
+ buttons=Gtk.ButtonsType.NONE,
+ text="")
dialog.set_markup(surrogate_escape(dialog_text))
dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.NO,
@@ -1108,6 +1233,9 @@
Left Click: Ignore.
Middle Click: Open the selected file.
Right Click: Show the popup menu."""
+ self.treeview_click_on = not self.treeview_click_on
+ if self.treeview_click_on:
+ return False
model, self.rows, self.selected_filenames = \
self.treeview_get_selected_rows(treeview)
@@ -1178,7 +1306,7 @@
def cell_data_func_filesize(self, column, cell_renderer,
tree_model, tree_iter, id):
"""File size cell display function."""
- if pyversion >= 3.0:
+ if helpers.check_python_version(3, 0):
size = int(tree_model.get_value(tree_iter, id))
else:
size = long(tree_model.get_value(tree_iter, id))
@@ -1357,7 +1485,7 @@
"""Try to fetch a thumbnail."""
thumbnails_directory = os.path.expanduser('~/.thumbnails/normal')
uri = 'file://' + path
- if pyversion >= 3.0:
+ if helpers.check_python_version(3, 0):
uri = uri.encode('utf-8')
md5_hash = hashlib.md5(uri).hexdigest()
thumbnail_path = os.path.join(
@@ -1432,12 +1560,9 @@
self.set_title(_("Searching for \"%s\"") % keywords)
self.spinner.show()
self.statusbar_label.set_label(_("Searching…"))
- self.search_entry.set_icon_from_stock(
- Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_STOP)
- self.search_entry.set_icon_tooltip_text(
- Gtk.EntryIconPosition.SECONDARY, _('Stop Search'))
self.search_in_progress = True
+ self.refresh_search_entry()
# Be thread friendly.
while Gtk.events_pending():
@@ -1451,9 +1576,10 @@
self.results_filter = model.filter_new()
self.results_filter.set_visible_func(self.results_filter_func)
sort = Gtk.TreeModelSort(model=self.results_filter)
- if pyversion >= 3.0:
+ if helpers.check_python_version(3, 0):
sort.set_sort_func(2, self.python_three_size_sort_func, None)
self.treeview.set_model(sort)
+ sort.get_model().get_model().clear()
self.treeview.columns_autosize()
# Enable multiple-selection
@@ -1477,7 +1603,7 @@
try:
path, name = os.path.split(filename)
- if pyversion >= 3.0:
+ if helpers.check_python_version(3, 0):
size = int(os.path.getsize(filename))
else:
size = long(os.path.getsize(filename))
@@ -1509,11 +1635,15 @@
continue
# Return to Non-Search Mode.
- self.get_window().set_cursor(None)
+ window = self.get_window()
+ if window is not None:
+ window.set_cursor(None)
self.set_title(_('Search results for \"%s\"') % keywords)
self.spinner.hide()
- n_results = len(self.treeview.get_model())
+ n_results = 0
+ if self.treeview.get_model() is not None:
+ n_results = len(self.treeview.get_model())
if n_results == 0:
self.statusbar_label.set_label(_("No files found."))
elif n_results == 1:
@@ -1522,20 +1652,7 @@
self.statusbar_label.set_label(_("%i files found.") % n_results)
self.search_in_progress = False
- if len(self.search_entry.get_text()) == 0:
- self.search_entry.set_icon_from_stock(
- Gtk.EntryIconPosition.SECONDARY,
- Gtk.STOCK_FIND)
- self.search_entry.set_icon_tooltip_text(
- Gtk.EntryIconPosition.SECONDARY,
- _('Enter search terms and press ENTER'))
- else:
- self.search_entry.set_icon_from_stock(
- Gtk.EntryIconPosition.SECONDARY,
- Gtk.STOCK_CLEAR)
- self.search_entry.set_icon_tooltip_text(
- Gtk.EntryIconPosition.SECONDARY,
- _('Clear search terms'))
+ self.refresh_search_entry()
self.stop_search = False
yield False
diff -Nru catfish-1.0.2/catfish/__init__.py catfish-1.2.2/catfish/__init__.py
--- catfish-1.0.2/catfish/__init__.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish/__init__.py 2014-09-21 21:34:50.000000000 +0000
@@ -31,7 +31,9 @@
def parse_options():
"""Support for command line options"""
- parser = optparse.OptionParser(version="catfish %s" % get_version())
+ usage = _("Usage: %prog [options] path query")
+ parser = optparse.OptionParser(version="catfish %s" % get_version(),
+ usage=usage)
parser.add_option(
"-v", "--verbose", action="count", dest="verbose",
help=_("Show debug messages (-vv debugs catfish_lib also)"))
@@ -43,22 +45,19 @@
parser.add_option('', '--iso-time', action='store_true',
dest='time_iso', help=_('Display time in ISO format'))
# Translators: Do not translate PATH, it is a variable.
- parser.add_option('', '--path', help=_('Search in folder PATH'))
- # Translators: Do not translate FILEMAN, it is a variable.
- parser.add_option('', '--fileman', help=_('Use FILEMAN as filemanager'))
- parser.add_option('', '--wrapper', metavar='WRAPPER',
- # Translators: Do not translate WRAPPER, it is a variable
- dest='open_wrapper', help=_('Use WRAPPER to open files'))
+ parser.add_option('', '--path', help=optparse.SUPPRESS_HELP)
parser.add_option('', '--exact', action='store_true',
help=_('Perform exact match'))
parser.add_option('', '--hidden', action='store_true',
help=_('Include hidden files'))
parser.add_option('', '--fulltext', action='store_true',
help=_('Perform fulltext search'))
+ parser.add_option('', '--start', action='store_true',
+ help=_("If path and query are provided, start searching "
+ "when the application is displayed."))
parser.set_defaults(icons_large=0, thumbnails=0, time_iso=0,
- path=os.path.expanduser('~'), fileman='xdg-open',
- exact=0, hidden=0, fulltext=0, file_action='open',
- open_wrapper='xdg-open')
+ path=None, start=False,
+ exact=0, hidden=0, fulltext=0, file_action='open')
(options, args) = parser.parse_args()
diff -Nru catfish-1.0.2/catfish.1 catfish-1.2.2/catfish.1
--- catfish-1.0.2/catfish.1 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish.1 2014-09-21 21:34:50.000000000 +0000
@@ -1,4 +1,4 @@
-.TH CATFISH "1" "February 2014" "catfish 1.0.1" "User Commands"
+.TH CATFISH "1" "August 2014" "catfish 1.2.0" "User Commands"
.SH NAME
catfish \- File searching tool which is configurable via the command line
.SH "DESCRIPTION"
@@ -6,6 +6,9 @@
is a handy file searching tool for Linux and UNIX.
The interface is intentionally lightweight and simple, using only Gtk+3.
You can configure it to your needs by using several command line options.
+.SH SYNOPSIS
+.B catfish
+[\fI\,options\/\fR] \fI\,path query\/\fR
.SH OPTIONS
.TP
\fB\-\-version\fR
@@ -26,15 +29,6 @@
\fB\-\-iso\-time\fR
Display time in ISO format
.TP
-\fB\-\-path\fR=\fIPATH\fR
-Search in folder PATH
-.TP
-\fB\-\-fileman\fR=\fIFILEMAN\fR
-Use FILEMAN as filemanager
-.TP
-\fB\-\-wrapper\fR=\fIWRAPPER\fR
-Use WRAPPER to open files
-.TP
\fB\-\-exact\fR
Perform exact match
.TP
@@ -42,4 +36,8 @@
Include hidden files
.TP
\fB\-\-fulltext\fR
-Perform fulltext search
\ No newline at end of file
+Perform fulltext search
+.TP
+\fB\-\-start\fR
+If path and query are provided, start searching when the
+application is displayed.
diff -Nru catfish-1.0.2/catfish_lib/AboutDialog.py catfish-1.2.2/catfish_lib/AboutDialog.py
--- catfish-1.0.2/catfish_lib/AboutDialog.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish_lib/AboutDialog.py 2014-09-21 21:34:50.000000000 +0000
@@ -19,6 +19,7 @@
from gi.repository import Gtk # pylint: disable=E0611
from . helpers import get_builder
+from . catfishconfig import get_version
class AboutDialog(Gtk.AboutDialog):
@@ -49,3 +50,4 @@
# Get a reference to the builder and set up the signals.
self.builder = builder
self.ui = builder.get_ui(self)
+ self.set_version(get_version())
diff -Nru catfish-1.0.2/catfish_lib/Builder.py catfish-1.2.2/catfish_lib/Builder.py
--- catfish-1.0.2/catfish_lib/Builder.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish_lib/Builder.py 2014-09-21 21:34:50.000000000 +0000
@@ -234,10 +234,10 @@
aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')]
# a method may have several aliases
- #~ @alias('on_btn_foo_clicked')
- #~ @alias('on_tool_foo_activate')
- #~ on_menu_foo_activate():
- #~ pass
+ # ~ @alias('on_btn_foo_clicked')
+ # ~ @alias('on_tool_foo_activate')
+ # ~ on_menu_foo_activate():
+ # ~ pass
alias_groups = [(x.aliases, x) for x in aliased_methods]
aliases = []
diff -Nru catfish-1.0.2/catfish_lib/catfishconfig.py catfish-1.2.2/catfish_lib/catfishconfig.py
--- catfish-1.0.2/catfish_lib/catfishconfig.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish_lib/catfishconfig.py 2014-09-21 21:34:50.000000000 +0000
@@ -26,7 +26,7 @@
# files). By default, this is ../data, relative your trunk layout
__catfish_data_directory__ = '../data/'
__license__ = 'GPL-2'
-__version__ = '1.0.2'
+__version__ = '1.2.2'
import os
diff -Nru catfish-1.0.2/catfish_lib/helpers.py catfish-1.2.2/catfish_lib/helpers.py
--- catfish-1.0.2/catfish_lib/helpers.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish_lib/helpers.py 2014-09-21 21:34:50.000000000 +0000
@@ -19,10 +19,33 @@
"""Helpers for an Ubuntu application."""
import logging
import os
+import sys
+from gi.repository import Gtk, GObject
from . catfishconfig import get_data_file
from . Builder import Builder
+python_version = sys.version_info[:3]
+gobject_version = GObject.pygobject_version
+gtk_version = (Gtk.get_major_version(),
+ Gtk.get_minor_version(),
+ Gtk.get_micro_version())
+
+
+def check_python_version(major_version, minor_version, micro=0):
+ """Return true if running python >= requested version"""
+ return python_version >= (major_version, minor_version, micro)
+
+
+def check_gtk_version(major_version, minor_version, micro=0):
+ """Return true if running gtk >= requested version"""
+ return gtk_version >= (major_version, minor_version, micro)
+
+
+def check_gobject_version(major_version, minor_version, micro=0):
+ """Return true if running gobject >= requested version"""
+ return gobject_version >= (major_version, minor_version, micro)
+
def get_builder(builder_file_name):
"""Return a fully-instantiated Gtk.Builder instance from specified ui file
diff -Nru catfish-1.0.2/catfish_lib/SudoDialog.py catfish-1.2.2/catfish_lib/SudoDialog.py
--- catfish-1.0.2/catfish_lib/SudoDialog.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish_lib/SudoDialog.py 2014-09-21 21:34:50.000000000 +0000
@@ -23,8 +23,70 @@
import pexpect
+gtk_version = (Gtk.get_major_version(),
+ Gtk.get_minor_version(),
+ Gtk.get_micro_version())
+
+
+def check_gtk_version(major_version, minor_version, micro=0):
+ """Return true if running gtk >= requested version"""
+ return gtk_version >= (major_version, minor_version, micro)
+
+# Check if the LANG variable needs to be set
+use_env = False
+
+
+def check_dependencies(commands=[]):
+ """Check for the existence of required commands, and sudo access"""
+ # Check for sudo
+ if pexpect.which("sudo") is None:
+ return False
+
+ # Check for required commands
+ for command in commands:
+ if pexpect.which(command) is None:
+ return False
+
+ # Check for LANG requirements
+ child = env_spawn('sudo -v', 1)
+ if child.expect([".*ssword.*", "Sorry",
+ pexpect.EOF,
+ pexpect.TIMEOUT]) == 3:
+ global use_env
+ use_env = True
+ child.close()
+
+ # Check for sudo rights
+ child = env_spawn('sudo -v', 1)
+ try:
+ index = child.expect([".*ssword.*", "Sorry",
+ pexpect.EOF, pexpect.TIMEOUT])
+ child.close()
+ if index == 0 or index == 2:
+ # User in sudoers, or already admin
+ return True
+ elif index == 1 or index == 3:
+ # User not in sudoers
+ return False
+
+ except:
+ # Something else went wrong.
+ child.close()
+
+ return False
+
+
+def env_spawn(command, timeout):
+ """Use pexpect.spawn, adapt for timeout and env requirements."""
+ if use_env:
+ child = pexpect.spawn(command, env={"LANG": "C"})
+ else:
+ child = pexpect.spawn(command)
+ child.timeout = timeout
+ return child
-class SudoDialog(Gtk.MessageDialog):
+
+class SudoDialog(Gtk.Dialog):
'''
Creates a new SudoDialog. This is a replacement for using gksudo which
provides additional flexibility when performing sudo commands.
@@ -44,33 +106,93 @@
- REJECT: Password invalid.
- ACCEPT: Password valid.
'''
- def __init__(self, parent=None, icon=None, message=None, name=None,
- retries=-1):
+ def __init__(self, title=None, parent=None, icon=None, message=None,
+ name=None, retries=-1):
"""Initialize the SudoDialog."""
- # default dialog parameters
- message_type = Gtk.MessageType.QUESTION
- buttons = Gtk.ButtonsType.NONE
-
# initialize the dialog
- super(SudoDialog, self).__init__(transient_for=parent,
- modal=True,
- destroy_with_parent=True,
- message_type=message_type,
- buttons=buttons,
- text='')
- self.set_dialog_icon(icon)
+ super(SudoDialog, self).__init__(title=title,
+ transient_for=parent,
+ modal=True,
+ destroy_with_parent=True)
+ #
self.connect("show", self.on_show)
+ if title is None:
+ title = _("Password Required")
+ self.set_title(title)
+
+ self.set_border_width(5)
- # add buttons
- button_box = self.get_children()[0].get_children()[1]
- self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
+ # Content Area
+ content_area = self.get_content_area()
+ grid = Gtk.Grid.new()
+ grid.set_row_spacing(6)
+ grid.set_column_spacing(12)
+ grid.set_margin_left(5)
+ grid.set_margin_right(5)
+ content_area.add(grid)
+
+ # Icon
+ self.dialog_icon = Gtk.Image.new_from_icon_name("dialog-password",
+ Gtk.IconSize.DIALOG)
+ grid.attach(self.dialog_icon, 0, 0, 1, 2)
+
+ # Text
+ self.primary_text = Gtk.Label.new("")
+ self.primary_text.set_use_markup(True)
+ self.primary_text.set_halign(Gtk.Align.START)
+ self.secondary_text = Gtk.Label.new("")
+ self.secondary_text.set_use_markup(True)
+ self.secondary_text.set_halign(Gtk.Align.START)
+ self.secondary_text.set_margin_top(6)
+ grid.attach(self.primary_text, 1, 0, 1, 1)
+ grid.attach(self.secondary_text, 1, 1, 1, 1)
+
+ # Infobar
+ self.infobar = Gtk.InfoBar.new()
+ self.infobar.set_margin_top(12)
+ self.infobar.set_message_type(Gtk.MessageType.WARNING)
+ content_area = self.infobar.get_content_area()
+ infobar_icon = Gtk.Image.new_from_icon_name("dialog-warning",
+ Gtk.IconSize.BUTTON)
+ label = Gtk.Label.new(_("Incorrect password... try again."))
+ content_area.add(infobar_icon)
+ content_area.add(label)
+ grid.attach(self.infobar, 0, 2, 2, 1)
+ content_area.show_all()
+ self.infobar.set_no_show_all(True)
+
+ # Password
+ label = Gtk.Label.new("")
+ label.set_use_markup(True)
+ label.set_markup("%s" % _("Password:"))
+ label.set_halign(Gtk.Align.START)
+ label.set_margin_top(12)
+ self.password_entry = Gtk.Entry()
+ self.password_entry.set_visibility(False)
+ self.password_entry.set_activates_default(True)
+ self.password_entry.set_margin_top(12)
+ grid.attach(label, 0, 3, 1, 1)
+ grid.attach(self.password_entry, 1, 3, 1, 1)
+
+ # Buttons
+ button = self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
+ button_box = button.get_parent()
+ button_box.set_margin_top(24)
ok_button = Gtk.Button.new_with_label(_("OK"))
ok_button.connect("clicked", self.on_ok_clicked)
ok_button.set_receives_default(True)
ok_button.set_can_default(True)
ok_button.set_sensitive(False)
self.set_default(ok_button)
- button_box.pack_start(ok_button, False, False, 0)
+ if check_gtk_version(3, 12):
+ button_box.pack_start(ok_button, True, True, 0)
+ else:
+ button_box.pack_start(ok_button, False, False, 0)
+
+ self.password_entry.connect("changed", self.on_password_changed,
+ ok_button)
+
+ self.set_dialog_icon(icon)
# add primary and secondary text
if message:
@@ -84,47 +206,11 @@
self.format_primary_text(primary_text)
self.format_secondary_text(secondary_text)
- # Pack the content area with password-related widgets.
- content_area = self.get_content_area()
-
- # Use an alignment to move align the password widgets with the text.
- self.password_alignment = Gtk.Alignment()
- # Make an educated guess about how for to align.
- left_align = Gtk.icon_size_lookup(Gtk.IconSize.DIALOG)[1] + 16
- self.password_alignment.set_padding(12, 12, left_align, 0)
-
- # Outer password box for incorrect password label and inner widgets.
- password_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
- spacing=12)
- password_outer.set_orientation(Gtk.Orientation.VERTICAL)
- # Password error label, only displayed when unsuccessful.
- self.password_info = Gtk.Label(label="")
- self.password_info.set_markup("%s" %
- _("Incorrect password... try again."))
-
- # Inner password box for Password: label and password entry.
- password_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
- spacing=12)
- password_label = Gtk.Label(label=_("Password:"))
- self.password_entry = Gtk.Entry()
- self.password_entry.set_visibility(False)
- self.password_entry.set_activates_default(True)
- self.password_entry.connect("changed", self.on_password_changed,
- ok_button)
-
- # Pack all the widgets.
- password_box.pack_start(password_label, False, False, 0)
- password_box.pack_start(self.password_entry, True, True, 0)
- password_outer.pack_start(self.password_info, True, True, 0)
- password_outer.pack_start(password_box, True, True, 0)
- self.password_alignment.add(password_outer)
- content_area.pack_start(self.password_alignment, True, True, 0)
- content_area.show_all()
- self.password_info.set_visible(False)
-
self.attempted_logins = 0
self.max_attempted_logins = retries
+ self.show_all()
+
def on_password_changed(self, widget, button):
"""Set the apply button sensitivity based on password input."""
button.set_sensitive(len(widget.get_text()) > 0)
@@ -132,11 +218,14 @@
def format_primary_text(self, message_format):
'''
Format the primary text widget.
+ '''
+ self.primary_text.set_markup("%s" % message_format)
- API extension to match with format_secondary_text.
+ def format_secondary_text(self, message_format):
+ '''
+ Format the secondary text widget.
'''
- label = self.get_message_area().get_children()[0]
- label.set_text(message_format)
+ self.secondary_text.set_markup(message_format)
def set_dialog_icon(self, icon=None):
'''
@@ -152,24 +241,21 @@
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon,
icon_size,
icon_size)
- image = Gtk.Image.new_from_pixbuf(pixbuf)
+ self.dialog_icon.set_from_pixbuf(pixbuf)
self.set_icon_from_file(icon)
else:
# icon is an named icon, so load it directly to an image
- image = Gtk.Image.new_from_icon_name(icon, icon_size)
+ self.dialog_icon.set_from_icon_name(icon, icon_size)
self.set_icon_name(icon)
else:
# fallback on password icon
- image = Gtk.Image.new_from_icon_name('dialog-password', icon_size)
+ self.dialog_icon.set_from_icon_name('dialog-password', icon_size)
self.set_icon_name('dialog-password')
- # align, show, and set the image.
- image.set_alignment(Gtk.Align.CENTER, Gtk.Align.FILL)
- image.show()
- self.set_image(image)
def on_show(self, widget):
'''When the dialog is displayed, clear the password.'''
self.set_password('')
+ self.password_valid = False
def on_ok_clicked(self, widget):
'''
@@ -179,25 +265,22 @@
If unsuccessful, try again until reaching maximum attempted logins,
then emit the response signal with REJECT.
'''
- top, bottom, left, right = self.password_alignment.get_padding()
if self.attempt_login():
self.password_valid = True
- # Adjust the dialog for attactiveness.
- self.password_alignment.set_padding(12, bottom, left, right)
- self.password_info.hide()
self.emit("response", Gtk.ResponseType.ACCEPT)
else:
self.password_valid = False
# Adjust the dialog for attactiveness.
- self.password_alignment.set_padding(0, bottom, left, right)
- self.password_info.show()
- self.set_password('')
+ self.infobar.show()
+ self.password_entry.grab_focus()
if self.attempted_logins == self.max_attempted_logins:
self.attempted_logins = 0
self.emit("response", Gtk.ResponseType.REJECT)
def get_password(self):
'''Return the currently entered password, or None if blank.'''
+ if not self.password_valid:
+ return None
password = self.password_entry.get_text()
if password == '':
return None
@@ -217,12 +300,11 @@
Return True if successful.
'''
# Set the pexpect variables and spawn the process.
- child = pexpect.spawn('sudo /bin/true', env={"LANG": "C"})
- child.timeout = 1
+ child = env_spawn('sudo /bin/true', 1)
try:
# Check for password prompt or program exit.
child.expect([".*ssword.*", pexpect.EOF])
- child.sendline(self.get_password())
+ child.sendline(self.password_entry.get_text())
child.expect(pexpect.EOF)
except pexpect.TIMEOUT:
# If we timeout, that means the password was unsuccessful.
diff -Nru catfish-1.0.2/catfish_lib/Window.py catfish-1.2.2/catfish_lib/Window.py
--- catfish-1.0.2/catfish_lib/Window.py 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/catfish_lib/Window.py 2014-09-21 21:34:50.000000000 +0000
@@ -67,7 +67,7 @@
button = Gtk.MenuButton()
button.set_size_request(32, 32)
image = Gtk.Image.new_from_icon_name("emblem-system-symbolic",
- Gtk.IconSize.MENU)
+ Gtk.IconSize.MENU)
button.set_image(image)
popup = builder.get_object('appmenu')
@@ -78,10 +78,6 @@
box.add(button)
button.show_all()
- # Help not currently in use.
- #def on_mnu_contents_activate(self, widget, data=None):
- # show_uri(self, "ghelp:%s" % get_help_uri())
-
def on_mnu_about_activate(self, widget, data=None):
"""Display the about box for catfish."""
if self.AboutDialog is not None:
diff -Nru catfish-1.0.2/ChangeLog catfish-1.2.2/ChangeLog
--- catfish-1.0.2/ChangeLog 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/ChangeLog 2014-09-21 21:34:50.000000000 +0000
@@ -1,3 +1,27 @@
+v1.2.2:
+ + Fix typo in CatfishWindow.py (LP: #1372166)
+ + Fix right-click activating previous item (LP: #1372165)
+ + Updated translations.
+
+v1.2.1:
+ + Fix regression introduced in 1.2.0 that prevents the application from
+ starting (Debian: #758652)
+
+v1.2.0:
+ + Added Search button to start search with the mouse (LP: #1300158)
+ + Added "--start" command line flag to start searching on startup
+ + Removed "--fileman" and "--wrapper" flags, xdg-open is sufficient
+ + Results are now cleared when starting a new search
+ + An InfoBar is now displayed when the locate database is out-of-date
+ + Improved updatedb and password dialogs
+ + Fixed searching for multiple terms (LP: #1353546)
+
+v1.0.3:
+ + Fix external file search methods (e.g. locate) (lp: #1329801)
+ + Improved handling for missing symbolic icons
+ + Add AppData files (lp: #1323747)
+ + Use LANG only when needed, disable updatedb if not in sudoers (lp: #1320777)
+
v1.0.2:
+ Update to latest SudoDialog
+ Cleanup remaining quickly template cruft
diff -Nru catfish-1.0.2/data/appdata/catfish.appdata.xml.in catfish-1.2.2/data/appdata/catfish.appdata.xml.in
--- catfish-1.0.2/data/appdata/catfish.appdata.xml.in 1970-01-01 00:00:00.000000000 +0000
+++ catfish-1.2.2/data/appdata/catfish.appdata.xml.in 2014-09-21 21:34:50.000000000 +0000
@@ -0,0 +1,96 @@
+
+
+
+ catfish.desktop
+ CC0-1.0
+ GPL-2.0+
+ Catfish
+ <_summary>Versatile file searching tool
+
+
+ <_p>
+ Catfish is a small, fast, and powerful file search utility. Featuring a
+ minimal interface with an emphasis on results, it helps users find the files
+ they need without a file manager. With powerful filters such as modification
+ date, file type, and file contents, users will no longer be dependent on the
+ file manager or organization skills.
+
+
+
+
+ http://screenshots.smdavis.us/catfish/catfish.png
+ http://screenshots.smdavis.us/catfish/catfish-filters.png
+
+
+ https://launchpad.net/catfish-search/
+ https://bugs.launchpad.net/catfish-search/
+ https://answers.launchpad.net/catfish-search
+
+ smd.seandavis@gmail.com
+
+
+ catfish
+
+
+
+
+
+ <_p>This release fixes two new bugs and includes updated translations.
+
+
+
+
+
+ <_p>This release fixes a regression where the application is unable
+ to start on some systems.
+
+
+
+
+
+ <_p>This release fixes a regression where multiple search terms were
+ no longer supported. An InfoBar is now displayed when the search
+ database is outdated, and the dialogs used to update the database
+ have been improved.
+
+
+
+
+
+
+ <_p>This release fixes two issues where locate would not be properly
+ executed and improves handling of missing symbolic icons.
+
+
+
+
+
+
+ <_p>This stable release improved the reliability of the password dialog,
+ cleaned up unused code, and fixed potential issues with the list and
+ item selection.
+
+
+
+
+
+
+ <_p>This release fixed a potential security issue with program startup
+ and fixed a regression with selecting multiple items.
+
+
+
+
+
+
+ <_p>The first release in the 1.0.x series introduced a refreshed
+ interface and fixed a number of long-standing bugs. Improvements to
+ the default permissions eliminated a number of warnings when packaging
+ for distributions. Accessibility was enhanced as all strings have been
+ made translatable and keyboard accelerators have been improved.
+
+
+
+
+
+
diff -Nru catfish-1.0.2/data/ui/AboutCatfishDialog.ui catfish-1.2.2/data/ui/AboutCatfishDialog.ui
--- catfish-1.0.2/data/ui/AboutCatfishDialog.ui 2014-03-09 13:30:42.000000000 +0000
+++ catfish-1.2.2/data/ui/AboutCatfishDialog.ui 2014-09-21 21:34:50.000000000 +0000
@@ -1,7 +1,8 @@
+
-
-
+
+