mrv.maya.ui.browse.control
Covered: 118 lines
Missed: 146 lines
Skipped 149 lines
Percent: 44 %
  2
"""Contains ui modules to build a finder-like browser for items of any kind"""
  3
__docformat__ = "restructuredtext"
  5
from interface import (iFinderProvider, iFinderFilter)
  7
import mrv.maya.ui as ui
  8
from mrv.maya.util import (OptionVarDict, logException )
  9
from util import concat_url
 11
from mrv.path import Path
 12
opts = OptionVarDict()
 14
__all__ = ( 'FileProvider', 'BookmarkControl',  
 15
			'FilePathControl', 'FileFilterControl', 'FinderElement', 
 16
			'FileStack', 'FileRootSelectorControl', 'FilePathControlEditable')
 22
class FileProvider(iFinderProvider):
 23
	"""Implements a provider for a file system"""
 24
	__slots__ = "_root"
 26
	def __init__(self, root):
 27
		super(FileProvider, self).__init__(root)
 28
		self._root = Path(self._root)
 30
	def formatItem(self, url_base, url_index, url_item):
 31
		return url_item
 33
	def urlItems(self, url):
 34
		"""Return directory items alphabetically, directories first"""
 35
		path = self._root / url
 36
		dirs, files = list(), list()
 38
		try:
 39
			for abs_path in path.listdir():
 40
				if abs_path.isdir():
 41
					dirs.append(abs_path)
 42
				else:
 43
					files.append(abs_path)
 46
			dirs.sort()
 47
			files.sort()
 48
			return [abspath.basename() for abspath in (dirs + files)] 
 49
		except OSError:
 51
			return list()
 55
class StackControlBase(ui.TextScrollList):
 56
	"""stack base implementation. A stack keeps multiple items which can be added 
 57
	and removed. Additionally, it allows to remap the items, effectively showing 
 58
	a formatted item, which is kept in sync with an unformatted item.
 59
	:note: for now, only adding items, format will be applied. All other methods
 60
		operate on the formatted items."""
 63
	def __init__(self, *args, **kwargs):
 64
		super(StackControlBase, self).__init__(*args, **kwargs)
 66
		self.base_items = list()
 70
	def formatItem(self, item):
 71
		""":return: formatted version of item"""
 72
		return item
 74
	def selectedUnformattedItem(self):
 75
		""":return: unformatted selected item or None"""
 76
		index = self.selectedIndex()
 77
		if index < 0:
 78
			return None
 79
		return self.base_items[index-1]
 81
	def selectUnformattedItem(self, index_or_item):
 82
		"""Select the unformatted item as identified by either the index or item
 83
		:param index_or_item: integer representing the 0-based index of the item to 
 84
			select, or the item's id
 85
		:raise ValueError: if the item does not exist"""
 86
		index = index_or_item
 87
		if not isinstance(index_or_item, int):
 88
			index = self.base_items.index(index_or_item)
 89
		self.p_selectIndexedItem = index+1
 95
	def removeItem(self, item):
 96
		"""Remove the given formatted item from the list, as well as the corresponding
 97
		unformtted item. Its not an error if the item does not exist"""
 98
		try:
 99
			index = self.items().index(item)
100
			del(self.base_items[index])
101
			super(StackControlBase, self).removeItem(item)
102
		except (ValueError, IndexError):
103
			pass
105
		return self
107
	def addItem(self, item):
108
		self.base_items.append(item)
109
		return super(StackControlBase, self).addItem(self.formatItem(item))
114
class FinderElement(StackControlBase):
115
	"""Element with special abilities to suite the finder better. This involves
116
	keeping a list of unformatted items which can be used as unique item identifiers.
118
	Set the items to a list of unique identifiers which represent the possibly different
119
	items actually present in the list."""
122
class FileStack(StackControlBase):
123
	"""Implements a stack which shows only the base names of files"""
127
	def formatItem(self, item):
128
		return Path(item).basename()
133
class FilePathControl(ui.TextField):
134
	"""Control displaying a relative url. If it is ediable, a filepath may be 
135
	entered and queried"""
138
	def path(self):
139
		""":return: string representing the currently active path"""
140
		return self.p_text
142
	def setPath(self, path):
143
		"""Set the control to display the given path"""
144
		if path is None:
145
			path = ''
146
		self.p_text = str(path)
148
	def setEditable(self, state):
149
		self.p_editable = state
151
	def editable(self):
152
		""":return: True if the control can be edited by the user"""
153
		return self.p_editable
157
class FilePathControlEditable(FilePathControl):
158
	"""A filepath control which tries to maintain changes applied by the user.
159
	It assumes that the system will use setPath to adjust the path, and checks
160
	for changes in the path string which will be reapplied to the newly set path
161
	if possible"""
163
	def __init__(self, *args, **kwargs):
164
		super(FilePathControlEditable, self).__init__(*args, **kwargs)
165
		self._prev_path = ''
167
	def setPath(self, path):
169
		if path is not None:
170
			appended_text = self.p_text.replace(self._prev_path, '', 1)
171
			self._prev_path = path
172
			path += appended_text
174
		super(FilePathControlEditable, self).setPath(path)
177
class BookmarkControl(StackControlBase):
178
	"""Control allowing to display a set of custom bookmarks, which are stored
179
	in optionVars"""
183
	k_bookmark_store = "MRV_bookmarks"
188
	bookmark_changed = ui.Signal()
191
	def __init__(self, *args, **kwargs):
194
		super(BookmarkControl, self).__init__(*args, **kwargs)
195
		self.setItems(self._unpack_stored_bookmarks())
196
		self.e_selectCommand = self._selection_changed
198
	def _parse_bookmark(self, bookmark):
199
		""":return: root,path tuple or raise"""
200
		root, path = None, None
201
		if isinstance(bookmark, tuple) and len(bookmark) == 2:
202
			root, path = bookmark
203
		else:
204
			bookmark = Path(bookmark)
205
			root = bookmark.root()
206
			root_with_sep = (root.endswith(root.sep) and root) or (root + root.sep)
207
			path = Path(bookmark.replace(root_with_sep, '', 1))
210
		return root, path
212
	def _unpack_stored_bookmarks(self):
213
		""":return: list of tuples of root,path pairs"""
214
		miter = iter(opts.get(self.k_bookmark_store, list()))
215
		return [item for item in zip(miter, miter)]
217
	def _store_item_list(self, items):
218
		"""Store a list of pairs"""
219
		flattened_list = list()
220
		for pair in items:
221
			flattened_list.extend(pair)
223
		opts[self.k_bookmark_store] = flattened_list
225
	def _store_bookmark(self, root, path, add=True):
226
		"""Store the given path under the given root
227
		:param add: if True, the path will be added to the bookmarks of the given 
228
			root, otherwise it will be removed"""
229
		items = self._unpack_stored_bookmarks()
230
		index_to_remove = None
231
		for index, (oroot, opath) in enumerate(items):
232
			if oroot == root and opath == path:
233
				if add:
234
					return
235
				else:
236
					index_to_remove = index
237
					break
242
		if add:
243
			items.append((root, path))
244
		else:
245
			if index_to_remove is None:
246
				return
248
			del(items[index_to_remove])
251
		self._store_item_list(items)
253
	@logException
254
	def _selection_changed(self, *args):
255
		"""Convert the default callback into our signals"""
256
		if not self.base_items:
257
			return
258
		root, path = self.base_items[self.selectedIndex()-1]
259
		self.bookmark_changed.send(root, path)
261
		self.setSelectedItem(None)
264
	def formatItem(self, root_path_tuple):
265
		if isinstance(root_path_tuple, tuple):
266
			return Path(root_path_tuple[-1]).basename()
267
		else:
268
			return Path(root_path_tuple).basename()
272
	def addItem(self, bookmark):
273
		"""Add a new bookmark
274
		:param bookmark: tuple of root,relative_path or a single absolute path. In the 
275
			latter case, the root will be the natural root of the absolute path"""
276
		root, path = self._parse_bookmark(bookmark)
277
		bm_formatted = self.formatItem((root, path))
279
		if bm_formatted in self.items():
280
			return
282
		self._store_bookmark(root, path, add=True)
283
		super(BookmarkControl, self).addItem((root, path))
286
	def setItems(self, bookmarks):
287
		"""Set this control to a list of bookmarks
288
		:param bookmarks: list of either tuples of (root, path) pairs or absolute paths
289
			whose root will be chosen automatically"""
290
		bms = list()
291
		self.base_items = list()
292
		for item in bookmarks:
293
			self.base_items.append(self._parse_bookmark(item))
294
			bms.append(self.formatItem(self.base_items[-1]))
296
		super(BookmarkControl, self).setItems(bms)
299
		del(opts[self.k_bookmark_store])
300
		self._store_item_list(self.base_items)
302
	def removeItem(self, bookmark):
303
		"""Remove the given bookmark from the list of bookmarks
304
		:param bookmark: full path to the bookmark to remove. Its not an error
305
			if it doesn't exist in the first place"""
306
		items = self.items()
307
		try:
308
			index = self.items().index(bookmark)
309
			root, path = self.base_items[index]
310
			super(BookmarkControl, self).removeItem(bookmark)
311
			self._store_bookmark(root, path, add=False)
312
		except (ValueError, IndexError):
313
			return
317
class FileRootSelectorControl(ui.TextScrollList):
318
	"""Keeps a list of possible roots which can be chosen. Each root is represented 
319
	by a Provider instance."""
323
	root_changed = ui.Signal()
326
	def __init__(self, *args, **kwargs):
327
		self._providers = list()
328
		self.e_selectCommand = self._selection_changed
330
	def _provider_by_root(self, root):
331
		""":return: provider instance having the given root, or None"""
332
		for p in self._providers:
333
			if p.root() == root:
334
				return p
337
		return None
339
	def setItems(self, providers):
340
		"""Set the given providers to be used by this instance
341
		:param providers: list of FileProvider instances"""
342
		for provider in providers:
343
			if not isinstance(provider, FileProvider):
344
				raise ValueError("Require %s instances" % FileProvider)
347
		self._providers = providers
348
		super(FileRootSelectorControl, self).setItems(p.root() for p in self._providers)
350
	def addItem(self, provider):
351
		"""Add the given provider to our list of provides"""
352
		super(FileRootSelectorControl, self).addItem(provider.root())
353
		self._providers.append(provider)
355
	def removeItem(self, provider):
356
		"""Remove the given provider from the list
357
		:param provider: FileProvider instance or root from which the provider
358
			can be determined"""
359
		if isinstance(provider, basestring):
360
			provider = self._provider_by_root(provider)
361
			if provider is None:
362
				return
366
		try:
367
			self._providers.remove(provider)
368
		except ValueError:
369
			return
370
		else:
371
			self.setItems(self._providers)
374
	def setSelectedItem(self, item):
375
		"""Fires a root_changed event if the item actually caused a selection change"""
376
		cur_index = self.selectedIndex()
377
		super(FileRootSelectorControl, self).setSelectedItem(item)
378
		if cur_index == self.selectedIndex():
379
			return
381
		self._selection_changed()
386
	def providers(self):
387
		""":return: list of currently used providers"""
388
		return list(self._providers)
394
	def _selection_changed(self, *args):
395
		index = self.selectedIndex()-1
396
		self.root_changed.send(self._providers[index])
403
class FileFilterControl(ui.FormLayout, iFinderFilter):
404
	"""Control providing a filter for finder urls which are file paths"""