diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js
index 5bf1618b0b8daba39ec19333af0e4864780dfefc..4334daa75561dfd32f1b209b1199696e2676b0e8 100644
--- a/apps/files/js/fileactions.js
+++ b/apps/files/js/fileactions.js
@@ -8,7 +8,6 @@
  *
  */
 
-/* global trashBinApp */
 (function() {
 
 	/**
@@ -109,33 +108,37 @@
 				permissions: permissions,
 				icon: icon,
 				actionHandler: action,
-				displayName: displayName
+				displayName: displayName || name
 			});
 		},
 		/**
 		 * Register action
 		 *
-		 * @param {Object} action action object
-		 * @param {String} action.name identifier of the action
-		 * @param {String} action.displayName display name of the action, defaults
-		 * to the name given in action.name
-		 * @param {String} action.mime mime type
-		 * @param {int} action.permissions permissions
-		 * @param {(Function|String)} action.icon icon path to the icon or function
-		 * that returns it
-		 * @param {OCA.Files.FileActions~actionHandler} action.actionHandler action handler function
+		 * @param {OCA.Files.FileAction} action object
 		 */
 		registerAction: function (action) {
 			var mime = action.mime;
 			var name = action.name;
+			var actionSpec = {
+				action: action.actionHandler,
+				name: name,
+				displayName: action.displayName,
+				mime: mime,
+				icon: action.icon,
+				permissions: action.permissions
+			};
+			if (_.isUndefined(action.displayName)) {
+				actionSpec.displayName = t('files', name);
+			}
+			if (_.isFunction(action.render)) {
+				actionSpec.render = action.render;
+			} else {
+				actionSpec.render = _.bind(this._defaultRenderAction, this);
+			}
 			if (!this.actions[mime]) {
 				this.actions[mime] = {};
 			}
-			this.actions[mime][name] = {
-				action: action.actionHandler,
-				permissions: action.permissions,
-				displayName: action.displayName || t('files', name)
-			};
+			this.actions[mime][name] = actionSpec;
 			this.icons[name] = action.icon;
 			this._notifyUpdateListeners('registerAction', {action: action});
 		},
@@ -212,6 +215,127 @@
 			var actions = this.get(mime, type, permissions);
 			return actions[name];
 		},
+		/**
+		 * Default function to render actions
+		 *
+		 * @param {OCA.Files.FileAction} actionSpec file action spec
+		 * @param {boolean} isDefault true if the action is a default one,
+		 * false otherwise
+		 * @param {OCA.Files.FileActionContext} context action context
+		 */
+		_defaultRenderAction: function(actionSpec, isDefault, context) {
+			var name = actionSpec.name;
+			if (name === 'Download' || !isDefault) {
+				var $actionLink = this._makeActionLink(actionSpec, context);
+				context.$file.find('a.name>span.fileactions').append($actionLink);
+				return $actionLink;
+			}
+		},
+		/**
+		 * Renders the action link element
+		 *
+		 * @param {OCA.Files.FileAction} actionSpec action object
+		 * @param {OCA.Files.FileActionContext} context action context
+		 */
+		_makeActionLink: function(actionSpec, context) {
+			var img = actionSpec.icon;
+			if (img && img.call) {
+				img = img(context.$file.attr('data-file'));
+			}
+			var html = '<a href="#">';
+			if (img) {
+				html += '<img class="svg" alt="" src="' + img + '" />';
+			}
+			if (actionSpec.displayName) {
+				html += '<span> ' + actionSpec.displayName + '</span></a>';
+			}
+
+			return $(html);
+		},
+		/**
+		 * Custom renderer for the "Rename" action.
+		 * Displays the rename action as an icon behind the file name.
+		 *
+		 * @param {OCA.Files.FileAction} actionSpec file action to render
+		 * @param {boolean} isDefault true if the action is a default action,
+		 * false otherwise
+		 * @param {OCAFiles.FileActionContext} context rendering context
+		 */
+		_renderRenameAction: function(actionSpec, isDefault, context) {
+			var $actionEl = this._makeActionLink(actionSpec, context);
+			var $container = context.$file.find('a.name span.nametext');
+			$container.find('.action-rename').remove();
+			$container.append($actionEl);
+			return $actionEl;
+		},
+		/**
+		 * Custom renderer for the "Delete" action.
+		 * Displays the "Delete" action as a trash icon at the end of
+		 * the table row.
+		 *
+		 * @param {OCA.Files.FileAction} actionSpec file action to render
+		 * @param {boolean} isDefault true if the action is a default action,
+		 * false otherwise
+		 * @param {OCAFiles.FileActionContext} context rendering context
+		 */
+		_renderDeleteAction: function(actionSpec, isDefault, context) {
+			var mountType = context.$file.attr('data-mounttype');
+			var deleteTitle = t('files', 'Delete');
+			if (mountType === 'external-root') {
+				deleteTitle = t('files', 'Disconnect storage');
+			} else if (mountType === 'shared-root') {
+				deleteTitle = t('files', 'Unshare');
+			}
+			var $actionLink = $('<a href="#" original-title="' +
+				escapeHTML(deleteTitle) +
+				'" class="action delete icon-delete" />'
+			);
+			var $container = context.$file.find('td:last');
+			$container.find('.delete').remove();
+			$container.append($actionLink);
+			return $actionLink;
+		},
+		/**
+		 * Renders the action element by calling actionSpec.render() and
+		 * registers the click event to process the action.
+		 *
+		 * @param {OCA.Files.FileAction} actionSpec file action to render
+		 * @param {boolean} isDefault true if the action is a default action,
+		 * false otherwise
+		 * @param {OCAFiles.FileActionContext} context rendering context
+		 */
+		_renderAction: function(actionSpec, isDefault, context) {
+			var $actionEl = actionSpec.render(actionSpec, isDefault, context);
+			if (!$actionEl || !$actionEl.length) {
+				return;
+			}
+			$actionEl.addClass('action action-' + actionSpec.name.toLowerCase());
+			$actionEl.attr('data-action', actionSpec.name);
+			$actionEl.on(
+				'click', {
+					a: null
+				},
+				function(event) {
+					var $file = $(event.target).closest('tr');
+					var currentFile = $file.find('td.filename');
+					var fileName = $file.attr('data-file');
+					event.stopPropagation();
+					event.preventDefault();
+
+					context.fileActions.currentFile = currentFile;
+					// also set on global object for legacy apps
+					window.FileActions.currentFile = currentFile;
+
+					actionSpec.action(
+						fileName,
+						_.extend(context, {
+							dir: $file.attr('data-path') || context.fileList.getCurrentDirectory()
+						})
+					);
+				}
+			);
+			return $actionEl;
+		},
 		/**
 		 * Display file actions for the given element
 		 * @param parent "td" element of the file for which to display actions
@@ -226,107 +350,51 @@
 				return;
 			}
 			this.currentFile = parent;
-			var $tr = parent.closest('tr');
 			var self = this;
-			var actions = this.getActions(this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions());
-			var file = this.getCurrentFile();
+			var $tr = parent.closest('tr');
+			var actions = this.getActions(
+				this.getCurrentMimeType(),
+				this.getCurrentType(),
+				this.getCurrentPermissions()
+			);
 			var nameLinks;
 			if ($tr.data('renaming')) {
 				return;
 			}
 
-			// recreate fileactions
+			// recreate fileactions container
 			nameLinks = parent.children('a.name');
 			nameLinks.find('.fileactions, .nametext .action').remove();
 			nameLinks.append('<span class="fileactions" />');
-			var defaultAction = this.getDefault(this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions());
-
-			var actionHandler = function (event) {
-				event.stopPropagation();
-				event.preventDefault();
+			var defaultAction = this.getDefault(
+				this.getCurrentMimeType(),
+				this.getCurrentType(),
+				this.getCurrentPermissions()
+			);
 
-				self.currentFile = event.data.elem;
-				// also set on global object for legacy apps
-				window.FileActions.currentFile = self.currentFile;
-
-				var file = self.getCurrentFile();
-				var $tr = $(this).closest('tr');
-
-				event.data.actionFunc(file, {
-					$file: $tr,
-					fileList: fileList,
-					fileActions: self,
-					dir: $tr.attr('data-path') || fileList.getCurrentDirectory()
-				});
-			};
-
-			var addAction = function (name, action, displayName) {
-
-				if ((name === 'Download' || action !== defaultAction) && name !== 'Delete') {
-
-					var img = self.icons[name],
-						actionText = displayName,
-						actionContainer = 'a.name>span.fileactions';
-
-					if (name === 'Rename') {
-						// rename has only an icon which appears behind
-						// the file name
-						actionText = '';
-						actionContainer = 'a.name span.nametext';
-					}
-					if (img.call) {
-						img = img(file);
-					}
-					var html = '<a href="#" class="action action-' + name.toLowerCase() + '" data-action="' + name + '">';
-					if (img) {
-						html += '<img class ="svg" alt="" src="' + img + '" />';
-					}
-					html += '<span> ' + actionText + '</span></a>';
-
-					var element = $(html);
-					element.data('action', name);
-					element.on('click', {a: null, elem: parent, actionFunc: actions[name].action}, actionHandler);
-					parent.find(actionContainer).append(element);
-				}
-
-			};
-
-			$.each(actions, function (name, action) {
+			$.each(actions, function (name, actionSpec) {
 				if (name !== 'Share') {
-					displayName = action.displayName;
-					ah = action.action;
-
-					addAction(name, ah, displayName);
+					self._renderAction(
+						actionSpec,
+						actionSpec.action === defaultAction, {
+							$file: $tr,
+							fileActions: this,
+							fileList : fileList
+						}
+					);
 				}
 			});
-			if(actions.Share){
-				displayName = t('files', 'Share');
-				addAction('Share', actions.Share, displayName);
-			}
-
-			// remove the existing delete action
-			parent.parent().children().last().find('.action.delete').remove();
-			if (actions['Delete']) {
-				var img = self.icons['Delete'];
-				var html;
-				var mountType = $tr.attr('data-mounttype');
-				var deleteTitle = t('files', 'Delete');
-				if (mountType === 'external-root') {
-					deleteTitle = t('files', 'Disconnect storage');
-				} else if (mountType === 'shared-root') {
-					deleteTitle = t('files', 'Unshare');
-				} else if (fileList.id === 'trashbin') {
-					deleteTitle = t('files', 'Delete permanently');
-				}
-
-				if (img.call) {
-					img = img(file);
-				}
-				html = '<a href="#" original-title="' + escapeHTML(deleteTitle) + '" class="action delete icon-delete" />';
-				var element = $(html);
-				element.data('action', actions['Delete']);
-				element.on('click', {a: null, elem: parent, actionFunc: actions['Delete'].action}, actionHandler);
-				parent.parent().children().last().append(element);
+			// added here to make sure it's always the last action
+			var shareActionSpec = actions.Share;
+			if (shareActionSpec){
+				this._renderAction(
+					shareActionSpec,
+					shareActionSpec.action === defaultAction, {
+						$file: $tr,
+						fileActions: this,
+						fileList: fileList
+					}
+				);
 			}
 
 			if (triggerEvent){
@@ -350,18 +418,34 @@
 		 * Register the actions that are used by default for the files app.
 		 */
 		registerDefaultActions: function() {
-			this.register('all', 'Delete', OC.PERMISSION_DELETE, function () {
-				return OC.imagePath('core', 'actions/delete');
-			}, function (filename, context) {
-				context.fileList.do_delete(filename, context.dir);
-				$('.tipsy').remove();
+			this.registerAction({
+				name: 'Delete',
+				displayName: '',
+				mime: 'all',
+				permissions: OC.PERMISSION_DELETE,
+				icon: function() {
+					return OC.imagePath('core', 'actions/delete');
+				},
+				render: _.bind(this._renderDeleteAction, this),
+				actionHandler: function(fileName, context) {
+					context.fileList.do_delete(fileName, context.dir);
+					$('.tipsy').remove();
+				}
 			});
 
 			// t('files', 'Rename')
-			this.register('all', 'Rename', OC.PERMISSION_UPDATE, function () {
-				return OC.imagePath('core', 'actions/rename');
-			}, function (filename, context) {
-				context.fileList.rename(filename);
+			this.registerAction({
+				name: 'Rename',
+				displayName: '',
+				mime: 'all',
+				permissions: OC.PERMISSION_UPDATE,
+				icon: function() {
+					return OC.imagePath('core', 'actions/rename');
+				},
+				render: _.bind(this._renderRenameAction, this),
+				actionHandler: function (filename, context) {
+					context.fileList.rename(filename);
+				}
 			});
 
 			this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
@@ -388,6 +472,47 @@
 
 	OCA.Files.FileActions = FileActions;
 
+	/**
+	 * File action attributes.
+	 *
+	 * @todo make this a real class in the future
+	 * @typedef {Object} OCA.Files.FileAction
+	 *
+	 * @property {String} name identifier of the action
+	 * @property {String} displayName display name of the action, defaults
+	 * to the name given in name property
+	 * @property {String} mime mime type
+	 * @property {int} permissions permissions
+	 * @property {(Function|String)} icon icon path to the icon or function
+	 * that returns it
+	 * @property {OCA.Files.FileActions~renderActionFunction} [render] optional rendering function
+	 * @property {OCA.Files.FileActions~actionHandler} actionHandler action handler function
+	 */
+
+	/**
+	 * File action context attributes.
+	 *
+	 * @typedef {Object} OCA.Files.FileActionContext
+	 *
+	 * @property {Object} $file jQuery file row element
+	 * @property {OCA.Files.FileActions} fileActions file actions object
+	 * @property {OCA.Files.FileList} fileList file list object
+	 */
+
+	/**
+	 * Render function for actions.
+	 * The function must render a link element somewhere in the DOM
+	 * and return it. The function should NOT register the event handler
+	 * as this will be done after the link was returned.
+	 *
+	 * @callback OCA.Files.FileActions~renderActionFunction
+	 * @param {OCA.Files.FileAction} actionSpec action definition
+	 * @param {Object} $row row container
+	 * @param {boolean} isDefault true if the action is the default one,
+	 * false otherwise
+	 * @return {Object} jQuery link object
+	 */
+
 	/**
 	 * Action handler function for file actions
 	 *
diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js
index f5f18a45a75d15c7b9a6a0cf83ab918e34e8241c..828aec9b6b948966a8c736ae1dc9305d4905b3ab 100644
--- a/apps/files/tests/js/fileactionsSpec.js
+++ b/apps/files/tests/js/fileactionsSpec.js
@@ -193,6 +193,54 @@ describe('OCA.Files.FileActions tests', function() {
 		context = actionStub.getCall(0).args[1];
 		expect(context.dir).toEqual('/somepath');
 	});
+	describe('custom rendering', function() {
+		var $tr;
+		beforeEach(function() {
+			var fileData = {
+				id: 18,
+				type: 'file',
+				name: 'testName.txt',
+				mimetype: 'text/plain',
+				size: '1234',
+				etag: 'a01234c',
+				mtime: '123456'
+			};
+			$tr = fileList.add(fileData);
+		});
+		it('regular function', function() {
+			var actionStub = sinon.stub();
+			FileActions.registerAction({
+				name: 'Test',
+				displayName: '',
+				mime: 'all',
+				permissions: OC.PERMISSION_READ,
+				render: function(actionSpec, isDefault, context) {
+					expect(actionSpec.name).toEqual('Test');
+					expect(actionSpec.displayName).toEqual('');
+					expect(actionSpec.permissions).toEqual(OC.PERMISSION_READ);
+					expect(actionSpec.mime).toEqual('all');
+					expect(isDefault).toEqual(false);
+
+					expect(context.fileList).toEqual(fileList);
+					expect(context.$file[0]).toEqual($tr[0]);
+
+					var $customEl = $('<a href="#"><span>blabli</span><span>blabla</span></a>');
+					$tr.find('td:first').append($customEl);
+					return $customEl;
+				},
+				actionHandler: actionStub
+			});
+			FileActions.display($tr.find('td.filename'), true, fileList);
+
+			var $actionEl = $tr.find('td:first .action-test');
+			expect($actionEl.length).toEqual(1);
+			expect($actionEl.hasClass('action')).toEqual(true);
+
+			$actionEl.click();
+			expect(actionStub.calledOnce).toEqual(true);
+			expect(actionStub.getCall(0).args[0]).toEqual('testName.txt');
+		});
+	});
 	describe('merging', function() {
 		var $tr;
 		beforeEach(function() {
diff --git a/apps/files_trashbin/js/app.js b/apps/files_trashbin/js/app.js
index a9727542bad3218bf599b02aeb0a62fab8cca1cf..72d9f4a6771f1bde6305736c88146c772c93018d 100644
--- a/apps/files_trashbin/js/app.js
+++ b/apps/files_trashbin/js/app.js
@@ -57,21 +57,34 @@ OCA.Trashbin.App = {
 			);
 		}, t('files_trashbin', 'Restore'));
 
-		fileActions.register('all', 'Delete', OC.PERMISSION_READ, function() {
-			return OC.imagePath('core', 'actions/delete');
-		}, function(filename, context) {
-			var fileList = context.fileList;
-			$('.tipsy').remove();
-			var tr = fileList.findFileEl(filename);
-			var deleteAction = tr.children("td.date").children(".action.delete");
-			deleteAction.removeClass('icon-delete').addClass('icon-loading-small');
-			fileList.disableActions();
-			$.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'), {
-					files: JSON.stringify([filename]),
-					dir: fileList.getCurrentDirectory()
-				},
-				_.bind(fileList._removeCallback, fileList)
-			);
+		fileActions.registerAction({
+			name: 'Delete',
+			displayName: '',
+			mime: 'all',
+			permissions: OC.PERMISSION_READ,
+			icon: function() {
+				return OC.imagePath('core', 'actions/delete');
+			},
+			render: function(actionSpec, isDefault, context) {
+				var $actionLink = fileActions._makeActionLink(actionSpec, context);
+				$actionLink.attr('original-title', t('files', 'Delete permanently'));
+				context.$file.find('td:last').append($actionLink);
+				return $actionLink;
+			},
+			actionHandler: function(filename, context) {
+				var fileList = context.fileList;
+				$('.tipsy').remove();
+				var tr = fileList.findFileEl(filename);
+				var deleteAction = tr.children("td.date").children(".action.delete");
+				deleteAction.removeClass('icon-delete').addClass('icon-loading-small');
+				fileList.disableActions();
+				$.post(OC.filePath('files_trashbin', 'ajax', 'delete.php'), {
+						files: JSON.stringify([filename]),
+						dir: fileList.getCurrentDirectory()
+					},
+					_.bind(fileList._removeCallback, fileList)
+				);
+			}
 		});
 		return fileActions;
 	}