[kupfer: 23/51] Create subpackage kupfer.obj
- From: Ulrik Sverdrup <usverdrup src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [kupfer: 23/51] Create subpackage kupfer.obj
- Date: Sun, 10 Jan 2010 11:58:46 +0000 (UTC)
commit 1a96fdbcaaa96776d0785216fbf1995a3b977e6f
Author: Ulrik Sverdrup <ulrik sverdrup gmail com>
Date: Sat Jan 9 20:00:51 2010 +0100
Create subpackage kupfer.obj
kupfer/core/data.py | 36 +-
kupfer/obj/apps.py | 45 ++
kupfer/obj/base.py | 391 ++++++++++++++
kupfer/obj/compose.py | 108 ++++
kupfer/{ => obj}/helplib.py | 0
kupfer/obj/objects.py | 580 ++++++++++++++++++++
kupfer/obj/sources.py | 168 ++++++
kupfer/objects.py | 1251 -------------------------------------------
kupfer/scheduler.py | 2 +-
9 files changed, 1311 insertions(+), 1270 deletions(-)
---
diff --git a/kupfer/core/data.py b/kupfer/core/data.py
index c61c6ae..d71c101 100644
--- a/kupfer/core/data.py
+++ b/kupfer/core/data.py
@@ -10,7 +10,7 @@ import operator
import gobject
gobject.threads_init()
-from kupfer import objects
+from kupfer.obj import base, sources, compose
from kupfer import config, pretty, scheduler, task
from kupfer import commandexec
from kupfer import datatools
@@ -72,14 +72,14 @@ class Searcher (object):
for src in sources:
fixedrank = 0
rankables = None
- if isinstance(src, objects.Source):
+ if isinstance(src, base.Source):
try:
# stored rankables
rankables = self._source_cache[src]
except KeyError:
# check uncached items
items = item_check(src.get_leaves())
- elif isinstance(src, objects.TextSource):
+ elif isinstance(src, base.TextSource):
items = item_check(src.get_items(key))
fixedrank = src.get_rank()
else:
@@ -94,7 +94,7 @@ class Searcher (object):
elif key:
rankables = search.score_objects(rankables, key)
matches = search.bonus_objects(rankables, key)
- if isinstance(src, objects.Source):
+ if isinstance(src, base.Source):
# we fork off a copy of the iterator to save
matches, self._source_cache[src] = itertools.tee(matches)
else:
@@ -259,7 +259,7 @@ class SourcePickler (pretty.OutputMixin):
return None
try:
source = pickle.loads(pfile.read())
- assert isinstance(source, objects.Source), "Stored object not a Source"
+ assert isinstance(source, base.Source), "Stored object not a Source"
sname = os.path.basename
self.output_debug("Loading", source, "from", sname(pickle_file))
except (pickle.PickleError, Exception), e:
@@ -344,7 +344,7 @@ class SourceController (pretty.OutputMixin):
root_catalog, = self.sources
elif len(self.sources) > 1:
firstlevel = self._pre_root
- root_catalog = objects.MultiSource(firstlevel)
+ root_catalog = sources.MultiSource(firstlevel)
else:
root_catalog = None
return root_catalog
@@ -352,12 +352,12 @@ class SourceController (pretty.OutputMixin):
@property
def _pre_root(self):
sourceindex = set(self.sources)
- kupfer_sources = objects.SourcesSource(self.sources)
+ kupfer_sources = sources.SourcesSource(self.sources)
sourceindex.add(kupfer_sources)
# Make sure firstlevel is ordered
# So that it keeps the ordering.. SourcesSource first
firstlevel = []
- firstlevel.append(objects.SourcesSource(sourceindex))
+ firstlevel.append(sources.SourcesSource(sourceindex))
firstlevel.extend(set(self.toplevel_sources))
return firstlevel
@@ -385,11 +385,11 @@ class SourceController (pretty.OutputMixin):
firstlevel = set()
# include the Catalog index since we want to include
# the top of the catalogs (like $HOME)
- catalog_index = (objects.SourcesSource(self.sources), )
+ catalog_index = (sources.SourcesSource(self.sources), )
for s in itertools.chain(self.sources, catalog_index):
if self.good_source_for_types(s, types):
firstlevel.add(s)
- return objects.MultiSource(firstlevel)
+ return sources.MultiSource(firstlevel)
def get_canonical_source(self, source):
"Return the canonical instance for @source"
@@ -425,7 +425,7 @@ class SourceController (pretty.OutputMixin):
contents = list(self.get_contents_for_leaf(obj, types))
content = contents and contents[0]
if len(contents) > 1:
- content = objects.SourcesSource(contents, name=unicode(obj),
+ content = sources.SourcesSource(contents, name=unicode(obj),
use_reprs=False)
obj.add_content(content)
@@ -709,7 +709,7 @@ class SecondaryObjectPane (LeafPane):
"""
self.latest_key = key
sources = []
- if not text_mode or isinstance(self.get_source(), objects.TextSource):
+ if not text_mode or isinstance(self.get_source(), base.TextSource):
sources.append(self.get_source())
if key and self.is_at_source_root():
# Only use text sources when we are at root catalog
@@ -845,11 +845,11 @@ class DataController (gobject.GObject, pretty.OutputMixin):
source_config = setctl.get_config
def dir_source(opt):
- return objects.DirectorySource(opt)
+ return sources.DirectorySource(opt)
def file_source(opt, depth=1):
abs = os.path.abspath(os.path.expanduser(opt))
- return objects.FileSource((abs,), depth)
+ return sources.FileSource((abs,), depth)
for coll, level in zip((s_sources, S_sources), ("Catalog", "Direct")):
for item in setctl.get_directories(level):
@@ -985,13 +985,13 @@ class DataController (gobject.GObject, pretty.OutputMixin):
self.cancel_search()
panectl.select(item)
if pane is SourcePane:
- assert not item or isinstance(item, objects.Leaf), \
+ assert not item or isinstance(item, base.Leaf), \
"Selection in Source pane is not a Leaf!"
# populate actions
self.action_pane.set_item(item)
self.search(ActionPane, interactive=True)
elif pane is ActionPane:
- assert not item or isinstance(item, objects.Action), \
+ assert not item or isinstance(item, base.Action), \
"Selection in Source pane is not an Action!"
if item and item.requires_object():
newmode = SourceActionObjectMode
@@ -1005,7 +1005,7 @@ class DataController (gobject.GObject, pretty.OutputMixin):
self.object_pane.set_item_and_action(self.source_pane.get_selection(), item)
self.search(ObjectPane, lazy=True)
elif pane is ObjectPane:
- assert not item or isinstance(item, objects.Leaf), \
+ assert not item or isinstance(item, base.Leaf), \
"Selection in Object pane is not a Leaf!"
def get_can_enter_text_mode(self, pane):
@@ -1106,7 +1106,7 @@ class DataController (gobject.GObject, pretty.OutputMixin):
return
else:
iobj = None
- obj = objects.ComposedLeaf(leaf, action, iobj)
+ obj = compose.ComposedLeaf(leaf, action, iobj)
self._insert_object(SourcePane, obj)
# pane cleared or set with new item
diff --git a/kupfer/obj/__init__.py b/kupfer/obj/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/kupfer/obj/apps.py b/kupfer/obj/apps.py
new file mode 100644
index 0000000..827cd78
--- /dev/null
+++ b/kupfer/obj/apps.py
@@ -0,0 +1,45 @@
+from kupfer.obj.base import InvalidDataError
+from kupfer.obj.objects import AppLeaf
+
+class AppLeafContentMixin (object):
+ """
+ Mixin for Source that correspond one-to-one with a AppLeaf
+
+ This Mixin sees to that the Source is set as content for the application
+ with id 'cls.appleaf_content_id', which may also be a sequence of ids.
+
+ Source has to define the attribute appleaf_content_id and must
+ inherit this mixin BEFORE the Source
+
+ This Mixin defines:
+ get_leaf_repr
+ decorates_type,
+ decorates_item
+ """
+ @classmethod
+ def get_leaf_repr(cls):
+ if not hasattr(cls, "_cached_leaf_repr"):
+ cls._cached_leaf_repr = cls.__get_leaf_repr()
+ return cls._cached_leaf_repr
+ @classmethod
+ def __get_appleaf_id_iter(cls):
+ if hasattr(cls.appleaf_content_id, "__iter__"):
+ ids = iter(cls.appleaf_content_id)
+ else:
+ ids = (cls.appleaf_content_id, )
+ return ids
+ @classmethod
+ def __get_leaf_repr(cls):
+ for appleaf_id in cls.__get_appleaf_id_iter():
+ try:
+ return AppLeaf(app_id=appleaf_id)
+ except InvalidDataError:
+ pass
+ @classmethod
+ def decorates_type(cls):
+ return AppLeaf
+ @classmethod
+ def decorate_item(cls, leaf):
+ if leaf == cls.get_leaf_repr():
+ return cls()
+
diff --git a/kupfer/obj/base.py b/kupfer/obj/base.py
new file mode 100644
index 0000000..60df0ff
--- /dev/null
+++ b/kupfer/obj/base.py
@@ -0,0 +1,391 @@
+from kupfer import datatools
+from kupfer import icons
+from kupfer import pretty
+from kupfer.utils import locale_sort
+from kupfer.kupferstring import tounicode, toutf8, tofolded
+
+__all__ = [
+ "InvalidDataError", "KupferObject", "Leaf", "Action", "Source", "TextSource"
+]
+
+class Error (Exception):
+ pass
+
+class InvalidDataError (Error):
+ """The data is wrong for the given Leaf"""
+ pass
+
+class InvalidLeafError (Error):
+ """The Leaf passed to an Action is invalid"""
+ pass
+
+class KupferObject (object):
+ """
+ Base class for kupfer data model
+
+ This class provides a way to get at an object's:
+
+ * icon with get_thumbnail, get_pixbuf and get_icon
+ * name with unicode() or str()
+ * description with get_description
+
+ @rank_adjust should be used _very_ sparingly:
+ Default actions should have +5 or +1
+ Destructive (dangerous) actions should have -5 or -10
+ """
+ rank_adjust = 0
+ def __init__(self, name=None):
+ """ Init kupfer object with, where
+ @name *should* be a unicode object but *may* be
+ a UTF-8 encoded `str`
+ """
+ if not name:
+ name = self.__class__.__name__
+ self.name = tounicode(name)
+ folded_name = tofolded(self.name)
+ self.name_aliases = set()
+ if folded_name != self.name:
+ self.name_aliases.add(folded_name)
+
+ def __str__(self):
+ return toutf8(self.name)
+
+ def __unicode__(self):
+ """Return a `unicode` representation of @self """
+ return self.name
+
+ def __repr__(self):
+ key = str(self.repr_key())
+ return "".join(("<", self.__module__, ".", self.__class__.__name__,
+ ((" %s" % key) if key else ""), ">"))
+
+ def repr_key(self):
+ """
+ Return an object whose str() will be used in the __repr__,
+ self is returned by default.
+ This value is used to recognize objects, for example learning commonly
+ used objects.
+ """
+ return self
+
+ def get_description(self):
+ """Return a description of the specific item
+ which *should* be a unicode object
+ """
+ return None
+
+ def get_thumbnail(self, width, height):
+ """Return pixbuf of size @width x @height if available
+ Most objects will not implement this
+ """
+ return None
+
+ def get_pixbuf(self, icon_size):
+ """
+ Returns an icon in pixbuf format with dimension @icon_size
+
+ Subclasses should implement: get_gicon and get_icon_name,
+ if they make sense.
+ The methods are tried in that order.
+ """
+ gicon = self.get_gicon()
+ if gicon:
+ pbuf = icons.get_icon_for_gicon(gicon, icon_size)
+ if pbuf:
+ return pbuf
+ icon_name = self.get_icon_name()
+ if icon_name:
+ icon = icons.get_icon_for_name(icon_name, icon_size)
+ if icon: return icon
+ return icons.get_icon_for_name(KupferObject.get_icon_name(self), icon_size)
+
+ def get_icon(self):
+ """
+ Returns an icon in GIcon format
+
+ Subclasses should implement: get_gicon and get_icon_name,
+ if they make sense.
+ The methods are tried in that order.
+ """
+ return icons.get_gicon_with_fallbacks(self.get_gicon(),
+ (self.get_icon_name(), KupferObject.get_icon_name(self)))
+
+ def get_gicon(self):
+ """Return GIcon, if there is one"""
+ return None
+
+ def get_icon_name(self):
+ """Return icon name. All items should have at least
+ a generic icon name to return.
+ """
+ return "kupfer-object"
+
+def aslist(seq):
+ """Return a list out of @seq, or seq if it is a list"""
+ if not isinstance(seq, type([])) and not isinstance(seq, type(())):
+ seq = list(seq)
+ return seq
+
+class Leaf (KupferObject):
+ """
+ Base class for objects
+
+ Leaf.object is the represented object (data)
+ All Leaves should be hashable (__hash__ and __eq__)
+ """
+ def __init__(self, obj, name):
+ """Represented object @obj and its @name"""
+ super(Leaf, self).__init__(name)
+ self.object = obj
+ self._has_content = None
+ self._content_source = None
+
+ def __hash__(self):
+ return hash(unicode(self))
+
+ def __eq__(self, other):
+ return (type(self) == type(other) and self.object == other.object)
+
+ def add_content(self, content):
+ """Register content source @content with Leaf"""
+ self._has_content = bool(content)
+ self._content_source = content
+
+ def has_content(self):
+ return self._has_content
+
+ def content_source(self, alternate=False):
+ """Content of leaf. it MAY alter behavior with @alternate,
+ as easter egg/extra mode"""
+ return self._content_source
+
+ def get_actions(self):
+ """Default (builtin) actions for this Leaf"""
+ return ()
+
+class Action (KupferObject):
+ '''
+ Base class for all actions
+
+ Implicit interface:
+ valid_object will be called once for each (secondary) object
+ to see if it applies. If it is not defined, all objects are
+ assumed ok (within the other type/source constraints)
+
+ def valid_object(self, obj, for_item):
+ """Whether @obj is good for secondary obj,
+ where @for_item is passed in as a hint for
+ which it should be applied to
+ """
+ return True
+ '''
+
+ def repr_key(self):
+ """by default, actions of one type are all the same"""
+ return ""
+
+ def activate(self, leaf, obj=None):
+ """Use this action with @leaf and @obj
+
+ @leaf: the object (Leaf)
+ @obj: an indirect object (Leaf), if self.requires_object
+ """
+ pass
+
+ def is_factory(self):
+ """Return whether action may return a result collection as a Source"""
+ return False
+
+ def has_result(self):
+ """Return whether action may return a result item as a Leaf"""
+ return False
+
+ def is_async(self):
+ """If this action runs asynchronously, return True.
+
+ Then activate(..) must return an object from the kupfer.task module,
+ which will be queued to run by Kupfer's task scheduler.
+ """
+ return False
+
+ def item_types(self):
+ """Yield types this action may apply to. This is used only
+ when this action is specified in __kupfer_actions__ to "decorate"
+ """
+ return ()
+
+ def valid_for_item(self, item):
+ """Whether action can be used with exactly @item"""
+ return True
+
+ def requires_object(self):
+ """If this action requires a secondary object
+ to complete is action, return True
+ """
+ return False
+
+ def object_source(self, for_item=None):
+ """Source to use for object or None,
+ to use the catalog (flat and filtered for @object_types)
+ """
+ return None
+
+ def object_types(self):
+ """Yield types this action may use as indirect objects, if the action
+ requrires it.
+ """
+ return ()
+
+ def get_icon_name(self):
+ return "gtk-execute"
+
+class Source (KupferObject, pretty.OutputMixin):
+ """
+ Source: Data provider for a kupfer browser
+
+ All Sources should be hashable and treated as equal if
+ their @repr are equal!
+
+ """
+ def __init__(self, name):
+ KupferObject.__init__(self, name)
+ self.cached_items = None
+ self._version = 1
+
+ @property
+ def version(self):
+ """version is for pickling (save and restore from cache),
+ subclasses should increase self._version when changing"""
+ return self._version
+
+ def __eq__(self, other):
+ return type(self) == type(other) and repr(self) == repr(other)
+
+ def __hash__(self ):
+ return hash(repr(self))
+
+ def toplevel_source(self):
+ return self
+
+ def initialize(self):
+ """
+ Called when a Source enters Kupfer's system for real
+
+ This method is called at least once for any "real" Source. A Source
+ must be able to return an icon name for get_icon_name as well as a
+ description for get_description, even if this method was never called.
+ """
+ pass
+
+ def repr_key(self):
+ # use the source's name so that it is reloaded on locale change
+ return (str(self), self.version)
+
+ def get_items(self):
+ """
+ Internal method to compute and return the needed items
+
+ Subclasses should use this method to return a sequence or
+ iterator to the leaves it contains
+ """
+ return []
+
+ def is_dynamic(self):
+ """
+ Whether to recompute contents each time it is accessed
+ """
+ return False
+
+ def mark_for_update(self):
+ """
+ Mark source as changed
+
+ it should be reloaded on next used (if normally cached)
+ """
+ self.cached_items = None
+
+ def should_sort_lexically(self):
+ """
+ Sources should return items by most relevant order (most
+ relevant first). If this is True, Source will sort items
+ from get_item() in locale lexical order
+ """
+ return False
+
+ def get_leaves(self, force_update=False):
+ """
+ Return a list of all leaves.
+
+ Subclasses should implement get_items, so that Source
+ can handle sorting and caching.
+ if @force_update, ignore cache, print number of items loaded
+ """
+ if self.should_sort_lexically():
+ # sort in locale order
+ sort_func = locale_sort
+ else:
+ sort_func = lambda x: x
+
+ if self.is_dynamic():
+ return sort_func(self.get_items())
+
+ if self.cached_items is None or force_update:
+ cache_type = aslist if force_update else datatools.SavedIterable
+ self.cached_items = cache_type(sort_func(self.get_items()))
+ if force_update:
+ self.output_info("Loaded %d items" % len(self.cached_items))
+ else:
+ self.output_debug("Loaded items")
+ return self.cached_items
+
+ def has_parent(self):
+ return False
+
+ def get_parent(self):
+ return None
+
+ def get_leaf_repr(self):
+ """Return, if appicable, another object
+ to take the source's place as Leaf"""
+ return None
+
+ def provides(self):
+ """A seq of the types of items it provides;
+ empty is taken as anything -- however most sources
+ should set this to exactly the type they yield
+ """
+ return ()
+
+class TextSource (KupferObject):
+ """TextSource base class implementation,
+
+ this is a psedo Source"""
+ def __init__(self, name=None):
+ if not name:
+ name = _("Text Matches")
+ KupferObject.__init__(self, name)
+
+ def __eq__(self, other):
+ return (type(self) == type(other) and repr(self).__eq__(repr(other)))
+
+ def __hash__(self ):
+ return hash(repr(self))
+
+ def initialize(self):
+ pass
+
+ def get_rank(self):
+ """All items are given this rank"""
+ return 20
+
+ def get_items(self, text):
+ """Get leaves for unicode string @text"""
+ return ()
+
+ def has_parent(self):
+ return False
+
+ def provides(self):
+ """A seq of the types of items it provides"""
+ yield Leaf
+
diff --git a/kupfer/obj/compose.py b/kupfer/obj/compose.py
new file mode 100644
index 0000000..513c8d7
--- /dev/null
+++ b/kupfer/obj/compose.py
@@ -0,0 +1,108 @@
+# encoding: utf-8
+
+from kupfer import icons
+from kupfer import pretty
+from kupfer import utils
+
+from kupfer.obj.base import Leaf, Action, Source, InvalidDataError
+from kupfer.obj.objects import Perform, RunnableLeaf, TextLeaf
+
+class TimedPerform (Perform):
+ """A timed proxy version of Perform
+
+ Proxy factory/result/async from a delegate action
+ Delay action by a couple of seconds
+ """
+ def __init__(self):
+ Action.__init__(self, _("Run After Delay..."))
+
+ def activate(self, leaf, iobj=None):
+ from kupfer import scheduler
+ # make a timer that will fire when Kupfer exits
+ interval = utils.parse_time_interval(iobj.object)
+ pretty.print_debug(__name__, "Run %s in %s seconds" % (leaf, interval))
+ timer = scheduler.Timer(True)
+ timer.set(interval, leaf.run)
+
+ def requires_object(self):
+ return True
+ def object_types(self):
+ yield TextLeaf
+
+ def valid_object(self, iobj, for_item=None):
+ interval = utils.parse_time_interval(iobj.object)
+ return interval > 0
+
+ def get_description(self):
+ return _("Perform command after a specified time interval")
+
+class ComposedLeaf (RunnableLeaf):
+ serilizable = True
+ def __init__(self, obj, action, iobj=None):
+ object_ = (obj, action, iobj)
+ # A slight hack: We remove trailing ellipsis and whitespace
+ format = lambda o: unicode(o).strip(".â?¦ ")
+ name = u" â?? ".join([format(o) for o in object_ if o is not None])
+ RunnableLeaf.__init__(self, object_, name)
+
+ def __getstate__(self):
+ from kupfer import puid
+ state = dict(vars(self))
+ state["object"] = [puid.get_unique_id(o) for o in self.object]
+ return state
+
+ def __setstate__(self, state):
+ from kupfer import puid
+ vars(self).update(state)
+ objid, actid, iobjid = state["object"]
+ obj = puid.resolve_unique_id(objid)
+ act = puid.resolve_action_id(actid, obj)
+ iobj = puid.resolve_unique_id(iobjid)
+ if (not obj or not act) or (iobj is None) != (iobjid is None):
+ raise InvalidDataError("Parts of %s not restored" % unicode(self))
+ self.object[:] = [obj, act, iobj]
+
+ def get_actions(self):
+ yield Perform()
+ yield TimedPerform()
+
+ def repr_key(self):
+ return self
+
+ def run(self):
+ from kupfer import commandexec
+ ctx = commandexec.DefaultActionExecutionContext()
+ obj, action, iobj = self.object
+ return ctx.run(obj, action, iobj, delegate=True)
+
+ def get_gicon(self):
+ obj, action, iobj = self.object
+ return icons.ComposedIcon(obj.get_icon(), action.get_icon())
+
+class _MultipleLeafContentSource (Source):
+ def __init__(self, leaf):
+ Source.__init__(self, unicode(leaf))
+ self.leaf = leaf
+ def get_items(self):
+ return self.leaf.object
+
+class MultipleLeaf (Leaf):
+ """
+ A Leaf representing a collection of leaves.
+
+ The represented object is a frozenset of the contained Leaves
+ """
+ def __init__(self, obj, name):
+ Leaf.__init__(self, frozenset(obj), name)
+
+ def has_content(self):
+ return True
+
+ def content_source(self, alternate=False):
+ return _MultipleLeafContentSource(self)
+
+ def get_description(self):
+ n = len(self.object)
+ return ngettext("%s object", "%s objects", n) % (n, )
+ def get_gicon(self):
+ pass
diff --git a/kupfer/helplib.py b/kupfer/obj/helplib.py
similarity index 100%
rename from kupfer/helplib.py
rename to kupfer/obj/helplib.py
diff --git a/kupfer/obj/objects.py b/kupfer/obj/objects.py
new file mode 100644
index 0000000..dedc800
--- /dev/null
+++ b/kupfer/obj/objects.py
@@ -0,0 +1,580 @@
+# -*- coding: UTF-8 -*-
+
+"""
+Copyright 2007--2009 Ulrik Sverdrup <ulrik sverdrup gmail com>
+
+This file is a part of the program kupfer, which is
+released under GNU General Public License v3 (or any later version),
+see the main program file, and COPYING for details.
+"""
+
+import os
+from os import path
+
+import gobject
+import gio
+
+from kupfer import icons, launch, utils
+from kupfer import pretty
+from kupfer.utils import locale_sort
+from kupfer.obj.base import Leaf, Action, Source, InvalidDataError
+from kupfer.obj.helplib import PicklingHelperMixin, FilesystemWatchMixin
+from kupfer.interface import TextRepresentation
+from kupfer.kupferstring import tounicode
+
+def ConstructFileLeafTypes():
+ """ Return a seq of the Leaf types returned by ConstructFileLeaf"""
+ yield FileLeaf
+ yield AppLeaf
+
+def ConstructFileLeaf(obj):
+ """
+ If the path in @obj points to a Desktop Item file,
+ return an AppLeaf, otherwise return a FileLeaf
+ """
+ root, ext = path.splitext(obj)
+ if ext == ".desktop":
+ try:
+ return AppLeaf(init_path=obj)
+ except InvalidDataError:
+ pass
+ return FileLeaf(obj)
+
+def _directory_content(dirpath, show_hidden):
+ from kupfer.obj.sources import DirectorySource
+ return DirectorySource(dirpath, show_hidden)
+
+class FileLeaf (Leaf, TextRepresentation):
+ """
+ Represents one file
+ """
+ serilizable = True
+ # To save memory with (really) many instances
+ __slots__ = ("name", "object")
+
+ def __init__(self, obj, name=None):
+ """Construct a FileLeaf
+
+ The display name of the file is normally derived from the full path,
+ and @name should normally be left unspecified.
+
+ @obj: byte string (file system encoding)
+ @name: unicode name or None for using basename
+ """
+ if obj is None:
+ raise InvalidDataError("File path for %s may not be None" % name)
+ # Use glib filename reading to make display name out of filenames
+ # this function returns a `unicode` object
+ if not name:
+ name = gobject.filename_display_basename(obj)
+ super(FileLeaf, self).__init__(obj, name)
+
+ def __eq__(self, other):
+ try:
+ return (type(self) == type(other) and
+ unicode(self) == unicode(other) and
+ path.samefile(self.object, other.object))
+ except OSError, exc:
+ pretty.print_debug(__name__, exc)
+ return False
+
+ def repr_key(self):
+ return self.object
+
+ def canonical_path(self):
+ """Return the true path of the File (without symlinks)"""
+ return path.realpath(self.object)
+
+ def is_valid(self):
+ return os.access(self.object, os.R_OK)
+
+ def _is_executable(self):
+ return os.access(self.object, os.R_OK | os.X_OK)
+
+ def is_dir(self):
+ return path.isdir(self.object)
+
+ def get_text_representation(self):
+ return gobject.filename_display_name(self.object)
+
+ def get_description(self):
+ """Format the path shorter:
+ replace homedir by ~/
+ """
+ return utils.get_display_path_for_bytestring(self.canonical_path())
+
+ def get_actions(self):
+ acts = [RevealFile(), ]
+ app_actions = []
+ default = None
+ if self.is_dir():
+ acts.append(OpenTerminal())
+ default = OpenDirectory()
+ elif self.is_valid():
+ gfile = gio.File(self.object)
+ info = gfile.query_info(gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE)
+ content_type = info.get_attribute_string(gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE)
+ def_app = gio.app_info_get_default_for_type(content_type, False)
+ def_key = def_app.get_id() if def_app else None
+ apps_for_type = gio.app_info_get_all_for_type(content_type)
+ apps = {}
+ for info in apps_for_type:
+ key = info.get_id()
+ if key not in apps:
+ try:
+ is_default = (key == def_key)
+ app = OpenWith(info, is_default)
+ apps[key] = app
+ except InvalidDataError:
+ pass
+ if def_key:
+ if not def_key in apps:
+ pretty.print_debug("No default found for %s, but found %s" % (self, apps))
+ else:
+ app_actions.append(apps.pop(def_key))
+ # sort the non-default OpenWith actions
+ open_with_sorted = locale_sort(apps.values())
+ app_actions.extend(open_with_sorted)
+
+ if self._is_executable():
+ acts.extend((Execute(), Execute(in_terminal=True)))
+ elif app_actions:
+ default = app_actions.pop(0)
+ else:
+ app_actions.append(Show())
+ if app_actions:
+ acts.extend(app_actions)
+ if default:
+ acts.insert(0, default)
+ return acts
+
+ def has_content(self):
+ return self.is_dir() or Leaf.has_content(self)
+ def content_source(self, alternate=False):
+ if self.is_dir():
+ return _directory_content(self.object, alternate)
+ else:
+ return Leaf.content_source(self)
+
+ def get_thumbnail(self, width, height):
+ if self.is_dir(): return None
+ return icons.get_thumbnail_for_file(self.object, width, height)
+ def get_gicon(self):
+ return icons.get_gicon_for_file(self.object)
+ def get_icon_name(self):
+ """A more generic icon"""
+ if self.is_dir():
+ return "folder"
+ else:
+ return "gtk-file"
+
+class SourceLeaf (Leaf):
+ def __init__(self, obj, name=None):
+ """Create SourceLeaf for source @obj"""
+ if not name:
+ name = unicode(obj)
+ Leaf.__init__(self, obj, name)
+ def has_content(self):
+ return True
+
+ def repr_key(self):
+ return repr(self.object)
+
+ def content_source(self, alternate=False):
+ return self.object
+
+ def get_description(self):
+ return self.object.get_description()
+
+ def get_gicon(self):
+ return self.object.get_gicon()
+
+ def get_icon_name(self):
+ return self.object.get_icon_name()
+
+class AppLeaf (Leaf, pretty.OutputMixin):
+ def __init__(self, item=None, init_path=None, app_id=None):
+ """Try constructing an Application for GAppInfo @item,
+ for file @path or for package name @app_id.
+ """
+ self.init_item = item
+ self.init_path = init_path
+ self.init_item_id = app_id and app_id + ".desktop"
+ # finish will raise InvalidDataError on invalid item
+ self.finish()
+ Leaf.__init__(self, self.object, self.object.get_name())
+ self.name_aliases.update(self._get_aliases())
+
+ def _get_aliases(self):
+ # find suitable alias
+ # use package name: non-extension part of ID
+ lowername = unicode(self).lower()
+ package_name = self._get_package_name()
+ if package_name and package_name not in lowername:
+ yield package_name
+
+ def __getstate__(self):
+ self.init_item_id = self.object and self.object.get_id()
+ state = dict(vars(self))
+ state["object"] = None
+ state["init_item"] = None
+ return state
+
+ def __setstate__(self, state):
+ vars(self).update(state)
+ self.finish()
+
+ def finish(self):
+ """Try to set self.object from init's parameters"""
+ item = None
+ if self.init_item:
+ item = self.init_item
+ else:
+ # Construct an AppInfo item from either path or item_id
+ from gio.unix import DesktopAppInfo, desktop_app_info_new_from_filename
+ if self.init_path and os.access(self.init_path, os.X_OK):
+ item = desktop_app_info_new_from_filename(self.init_path)
+ try:
+ # try to annotate the GAppInfo object
+ item.init_path = self.init_path
+ except AttributeError, exc:
+ self.output_debug(exc)
+ elif self.init_item_id:
+ try:
+ item = DesktopAppInfo(self.init_item_id)
+ except RuntimeError:
+ self.output_debug(self, "Application", self.init_item_id,
+ "not found")
+ self.object = item
+ if not self.object:
+ raise InvalidDataError
+
+ def repr_key(self):
+ return self.get_id()
+
+ def _get_package_name(self):
+ return os.path.basename(self.get_id())
+
+ def get_id(self):
+ """Return the unique ID for this app.
+
+ This is the GIO id "gedit.desktop" minus the .desktop part for
+ system-installed applications.
+ """
+ return launch.application_id(self.object)
+
+ def get_actions(self):
+ if launch.application_is_running(self.object):
+ yield Launch(_("Go To"), is_running=True)
+ yield CloseAll()
+ else:
+ yield Launch()
+ yield LaunchAgain()
+
+ def get_description(self):
+ # Use Application's description, else use executable
+ # for "file-based" applications we show the path
+ app_desc = tounicode(self.object.get_description())
+ ret = tounicode(app_desc if app_desc else self.object.get_executable())
+ if self.init_path:
+ app_path = utils.get_display_path_for_bytestring(self.init_path)
+ return u"(%s) %s" % (app_path, ret)
+ return ret
+
+ def get_gicon(self):
+ return self.object.get_icon()
+
+ def get_icon_name(self):
+ return "exec"
+
+class OpenWith (Action):
+ """
+ Open a FileLeaf with a specified application
+ """
+
+ def __init__(self, desktop_item, is_default=False):
+ """
+ Construct an "Open with application" item:
+
+ Application of @name should open, if
+ @is_default, it means it is the default app and
+ should only be styled "Open"
+ """
+ if not desktop_item:
+ raise InvalidDataError
+
+ name = desktop_item.get_name()
+ action_name = _("Open") if is_default else _("Open with %s") % name
+ Action.__init__(self, action_name)
+ self.desktop_item = desktop_item
+ self.is_default = is_default
+
+ # add a name alias from the package name of the application
+ if is_default:
+ self.rank_adjust = 5
+ self.name_aliases.add(_("Open with %s") % name)
+ package_name, ext = path.splitext(self.desktop_item.get_id() or "")
+ if package_name:
+ self.name_aliases.add(_("Open with %s") % package_name)
+
+ def repr_key(self):
+ return "" if self.is_default else self.desktop_item.get_id()
+
+ def activate(self, leaf):
+ if not self.desktop_item.supports_files() and not self.desktop_item.supports_uris():
+ pretty.print_error(__name__, self.desktop_item,
+ "says it does not support opening files, still trying to open")
+ utils.launch_app(self.desktop_item, paths=(leaf.object,))
+
+ def get_description(self):
+ if self.is_default:
+ return _("Open with %s") % self.desktop_item.get_name()
+ else:
+ # no description is better than a duplicate title
+ #return _("Open with %s") % self.desktop_item.get_name()
+ return u""
+ def get_gicon(self):
+ return icons.ComposedIcon(self.get_icon_name(),
+ self.desktop_item.get_icon(), emblem_is_fallback=True)
+ def get_icon_name(self):
+ return "gtk-execute"
+
+class OpenUrl (Action):
+ rank_adjust = 5
+ def __init__(self, name=None):
+ """
+ open url
+ """
+ if not name:
+ name = _("Open URL")
+ super(OpenUrl, self).__init__(name)
+
+ def activate(self, leaf):
+ url = leaf.object
+ self.open_url(url)
+
+ def open_url(self, url):
+ utils.show_url(url)
+
+ def get_description(self):
+ return _("Open URL with default viewer")
+
+ def get_icon_name(self):
+ return "forward"
+
+class Show (Action):
+ """ Open file with default viewer """
+ rank_adjust = 5
+ def __init__(self, name=_("Open")):
+ super(Show, self).__init__(name)
+
+ def activate(self, leaf):
+ utils.show_path(leaf.object)
+
+ def get_description(self):
+ return _("Open with default viewer")
+
+ def get_icon_name(self):
+ return "gtk-execute"
+
+class OpenDirectory (Show):
+ rank_adjust = 5
+ def __init__(self):
+ super(OpenDirectory, self).__init__(_("Open"))
+
+ def get_description(self):
+ return _("Open folder")
+
+ def get_icon_name(self):
+ return "folder-open"
+
+class RevealFile (Action):
+ def __init__(self, name=_("Reveal")):
+ super(RevealFile, self).__init__(name)
+
+ def activate(self, leaf):
+ fileloc = leaf.object
+ parent = path.normpath(path.join(fileloc, path.pardir))
+ utils.show_path(parent)
+
+ def get_description(self):
+ return _("Open parent folder")
+
+ def get_icon_name(self):
+ return "folder-open"
+
+class OpenTerminal (Action):
+ def __init__(self, name=_("Open Terminal Here")):
+ super(OpenTerminal, self).__init__(name)
+
+ def activate(self, leaf):
+ # any: take first successful command
+ any(utils.spawn_async((term, ), in_dir=leaf.object) for term in
+ ("xdg-terminal", "gnome-terminal", "xterm"))
+
+ def get_description(self):
+ return _("Open this location in a terminal")
+ def get_icon_name(self):
+ return "terminal"
+
+class Launch (Action):
+ """
+ Launch operation base class
+
+ Launches an application (AppLeaf)
+ """
+ rank_adjust = 5
+ def __init__(self, name=None, is_running=False, open_new=False):
+ if not name:
+ name = _("Launch")
+ Action.__init__(self, name)
+ self.is_running = is_running
+ self.open_new = open_new
+
+ def activate(self, leaf):
+ desktop_item = leaf.object
+ launch.launch_application(leaf.object, activate=not self.open_new)
+
+ def get_description(self):
+ if self.is_running:
+ return _("Show application window")
+ return _("Launch application")
+
+ def get_icon_name(self):
+ if self.is_running:
+ return "gtk-jump-to-ltr"
+ return Action.get_icon_name(self)
+
+class LaunchAgain (Launch):
+ """Launch instance without checking if running"""
+ rank_adjust = 0
+ def __init__(self, name=None):
+ if not name:
+ name = _("Launch Again")
+ Launch.__init__(self, name, open_new=True)
+ def item_types(self):
+ yield AppLeaf
+ def valid_for_item(self, leaf):
+ return launch.application_is_running(leaf.object)
+ def get_description(self):
+ return _("Launch another instance of this application")
+
+class CloseAll (Action):
+ """Attept to close all application windows"""
+ rank_adjust = -10
+ def __init__(self):
+ Action.__init__(self, _("Close"))
+ def activate(self, leaf):
+ return launch.application_close_all(leaf.object)
+ def item_types(self):
+ yield AppLeaf
+ def valid_for_item(self, leaf):
+ return launch.application_is_running(leaf.object)
+ def get_description(self):
+ return _("Attept to close all application windows")
+ def get_icon_name(self):
+ return "gtk-close"
+
+class Execute (Launch):
+ """
+ Execute executable file (FileLeaf)
+ """
+ rank_adjust = 5
+ def __init__(self, in_terminal=False, quoted=True):
+ name = _("Run in Terminal") if in_terminal else _("Run")
+ super(Execute, self).__init__(name)
+ self.in_terminal = in_terminal
+ self.quoted = quoted
+
+ def repr_key(self):
+ return (self.in_terminal, self.quoted)
+
+ def activate(self, leaf):
+ cmd = "'%s'" % leaf.object if self.quoted else leaf.object
+ utils.launch_commandline(cmd, in_terminal=self.in_terminal)
+
+ def get_description(self):
+ if self.in_terminal:
+ return _("Run this program in a Terminal")
+ else:
+ return _("Run this program")
+
+class UrlLeaf (Leaf, TextRepresentation):
+ # slots saves memory since we have lots this Leaf
+ __slots__ = ("name", "object")
+ def __init__(self, obj, name):
+ super(UrlLeaf, self).__init__(obj, name)
+
+ def get_actions(self):
+ return (OpenUrl(), )
+
+ def get_description(self):
+ return self.object
+
+ def get_icon_name(self):
+ return "text-html"
+
+class RunnableLeaf (Leaf):
+ """Leaf where the Leaf is basically the action itself,
+ for items such as Quit, Log out etc. Is executed by the
+ only action Perform
+ """
+ def __init__(self, obj=None, name=None):
+ Leaf.__init__(self, obj, name)
+ def get_actions(self):
+ yield Perform()
+ def run(self):
+ raise NotImplementedError
+ def repr_key(self):
+ return ""
+ def get_gicon(self):
+ iname = self.get_icon_name()
+ if iname:
+ return icons.get_gicon_with_fallbacks(None, (iname, ))
+ return icons.ComposedIcon("kupfer-object", "gtk-execute")
+ def get_icon_name(self):
+ return ""
+
+class Perform (Action):
+ """Perform the action in a RunnableLeaf"""
+ rank_adjust = 5
+ def __init__(self, name=None):
+ if not name: name = _("Perform")
+ super(Perform, self).__init__(name=name)
+ def activate(self, leaf):
+ return leaf.run()
+ def get_description(self):
+ return _("Carry out command")
+
+class TextLeaf (Leaf, TextRepresentation):
+ """Represent a text query
+ represented object is the unicode string
+ """
+ serilizable = True
+ def __init__(self, text, name=None):
+ """@text *must* be unicode or UTF-8 str"""
+ text = tounicode(text)
+ if not name:
+ lines = [l for l in text.splitlines() if l.strip()]
+ name = lines[0] if lines else text
+ Leaf.__init__(self, text, name)
+
+ def get_actions(self):
+ return ()
+
+ def repr_key(self):
+ return hash(self.object)
+
+ def get_description(self):
+ lines = [l for l in self.object.splitlines() if l.strip()]
+ desc = lines[0] if lines else self.object
+ numlines = len(lines) or 1
+
+ # TRANS: This is description for a TextLeaf, a free-text search
+ # TRANS: The plural parameter is the number of lines %(num)d
+ return ngettext('"%(text)s"', '(%(num)d lines) "%(text)s"',
+ numlines) % {"num": numlines, "text": desc }
+
+ def get_icon_name(self):
+ return "gtk-select-all"
+
diff --git a/kupfer/obj/sources.py b/kupfer/obj/sources.py
new file mode 100644
index 0000000..4f5dd1e
--- /dev/null
+++ b/kupfer/obj/sources.py
@@ -0,0 +1,168 @@
+import itertools
+import os
+from os import path
+
+import gobject
+
+from kupfer import datatools
+from kupfer import icons
+from kupfer import utils
+
+from kupfer.obj.base import Leaf, Action, Source
+from kupfer.obj.helplib import PicklingHelperMixin, FilesystemWatchMixin
+from kupfer.obj.objects import FileLeaf, AppLeaf, SourceLeaf
+from kupfer.obj.objects import ConstructFileLeaf, ConstructFileLeafTypes
+
+
+class FileSource (Source):
+ def __init__(self, dirlist, depth=0):
+ """
+ @dirlist: Directories as byte strings
+ """
+ name = gobject.filename_display_basename(dirlist[0])
+ if len(dirlist) > 1:
+ name = _("%s et. al.") % name
+ super(FileSource, self).__init__(name)
+ self.dirlist = dirlist
+ self.depth = depth
+
+ def __repr__(self):
+ return "%s.%s((%s, ), depth=%d)" % (self.__class__.__module__,
+ self.__class__.__name__,
+ ', '.join('"%s"' % d for d in sorted(self.dirlist)), self.depth)
+
+ def get_items(self):
+ iters = []
+
+ def mkleaves(directory):
+ files = utils.get_dirlist(directory, depth=self.depth,
+ exclude=self._exclude_file)
+ return (ConstructFileLeaf(f) for f in files)
+
+ for d in self.dirlist:
+ iters.append(mkleaves(d))
+
+ return itertools.chain(*iters)
+
+ def should_sort_lexically(self):
+ return True
+
+ def _exclude_file(self, filename):
+ return filename.startswith(".")
+
+ def get_description(self):
+ return (_("Recursive source of %(dir)s, (%(levels)d levels)") %
+ {"dir": self.name, "levels": self.depth})
+
+ def get_icon_name(self):
+ return "folder-saved-search"
+ def provides(self):
+ return ConstructFileLeafTypes()
+
+class DirectorySource (Source, PicklingHelperMixin, FilesystemWatchMixin):
+ def __init__(self, dir, show_hidden=False):
+ # Use glib filename reading to make display name out of filenames
+ # this function returns a `unicode` object
+ name = gobject.filename_display_basename(dir)
+ super(DirectorySource, self).__init__(name)
+ self.directory = dir
+ self.show_hidden = show_hidden
+ self.unpickle_finish()
+
+ def __repr__(self):
+ return "%s.%s(\"%s\", show_hidden=%s)" % (self.__class__.__module__,
+ self.__class__.__name__, str(self.directory), self.show_hidden)
+
+ def unpickle_finish(self):
+ self.monitor = self.monitor_directories(self.directory)
+
+ def get_items(self):
+ try:
+ for fname in os.listdir(self.directory):
+ if self.show_hidden or not fname.startswith("."):
+ yield ConstructFileLeaf(path.join(self.directory, fname))
+ except OSError, exc:
+ self.output_error(exc)
+
+ def should_sort_lexically(self):
+ return True
+
+ def _parent_path(self):
+ return path.normpath(path.join(self.directory, path.pardir))
+
+ def has_parent(self):
+ return not path.samefile(self.directory , self._parent_path())
+
+ def get_parent(self):
+ if not self.has_parent():
+ return super(DirectorySource, self).has_parent(self)
+ return DirectorySource(self._parent_path())
+
+ def get_description(self):
+ return _("Directory source %s") % self.directory
+
+ def get_gicon(self):
+ return icons.get_gicon_for_file(self.directory)
+
+ def get_icon_name(self):
+ return "folder"
+
+ def get_leaf_repr(self):
+ return FileLeaf(self.directory)
+ def provides(self):
+ return ConstructFileLeafTypes()
+
+class SourcesSource (Source):
+ """ A source whose items are SourceLeaves for @source """
+ def __init__(self, sources, name=None, use_reprs=True):
+ if not name: name = _("Catalog Index")
+ super(SourcesSource, self).__init__(name)
+ self.sources = sources
+ self.use_reprs = use_reprs
+
+ def get_items(self):
+ """Ask each Source for a Leaf substitute, else
+ yield a SourceLeaf """
+ for s in self.sources:
+ yield (self.use_reprs and s.get_leaf_repr()) or SourceLeaf(s)
+
+ def should_sort_lexically(self):
+ return True
+
+ def get_description(self):
+ return _("An index of all available sources")
+
+ def get_icon_name(self):
+ return "folder-saved-search"
+
+class MultiSource (Source):
+ """
+ A source whose items are the combined items
+ of all @sources
+ """
+ def __init__(self, sources):
+ super(MultiSource, self).__init__(_("Catalog"))
+ self.sources = sources
+
+ def is_dynamic(self):
+ """
+ MultiSource should be dynamic so some of its content
+ also can be
+ """
+ return True
+
+ def get_items(self):
+ iterators = []
+ ui = datatools.UniqueIterator(S.toplevel_source() for S in self.sources)
+ for S in ui:
+ it = S.get_leaves()
+ iterators.append(it)
+
+ return itertools.chain(*iterators)
+
+ def get_description(self):
+ return _("Root catalog")
+
+ def get_icon_name(self):
+ return "folder-saved-search"
+
diff --git a/kupfer/scheduler.py b/kupfer/scheduler.py
index 41dc358..9efe61e 100644
--- a/kupfer/scheduler.py
+++ b/kupfer/scheduler.py
@@ -2,7 +2,7 @@
import gobject
from kupfer import pretty
-from kupfer.helplib import gobject_connect_weakly
+from kupfer.obj.helplib import gobject_connect_weakly
_scheduler = None
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]