Index: /quintagroup.plonetabs/trunk/README.txt
===================================================================
--- /quintagroup.plonetabs/trunk/README.txt	(revision 540)
+++ /quintagroup.plonetabs/trunk/README.txt	(revision 540)
@@ -0,0 +1,22 @@
+quintagroup.plonetabs Package Readme
+=========================
+
+Overview
+--------
+
+The product allows Plone websites to manage portal_actions tool
+  from portal_control_panel configlet.
+
+  (c) "Quintagroup":http://quintagroup.com/ , 2006 - 2008.
+
+  support@quintagroup.com * quintessence of modern business
+
+Requirements:
+
+  * Plone 3.0+
+
+Tested on:
+
+  * Firefox
+
+  * IE 6,7
Index: /quintagroup.plonetabs/trunk/docs/HISTORY.txt
===================================================================
--- /quintagroup.plonetabs/trunk/docs/HISTORY.txt	(revision 540)
+++ /quintagroup.plonetabs/trunk/docs/HISTORY.txt	(revision 540)
@@ -0,0 +1,9 @@
+Changelog for quintagroup.plonetabs
+
+    (name of developer listed in brackets)
+
+quintagroup.plonetabs - 0.5 Unreleased
+
+    - Initial package structure.
+      [zopeskel]
+
Index: /quintagroup.plonetabs/trunk/docs/INSTALL.txt
===================================================================
--- /quintagroup.plonetabs/trunk/docs/INSTALL.txt	(revision 540)
+++ /quintagroup.plonetabs/trunk/docs/INSTALL.txt	(revision 540)
@@ -0,0 +1,43 @@
+quintagroup.plonetabs Installation
+==========================
+
+To install quintagroup.plonetabs into the global Python environment (or a workingenv),
+using a traditional Zope 2 instance, you can do this:
+
+ * When you're reading this you have probably already run 
+   ``easy_install quintagroup.plonetabs``. Find out how to install setuptools
+   (and EasyInstall) here:
+   http://peak.telecommunity.com/DevCenter/EasyInstall
+
+ * Create a file called ``quintagroup.plonetabs-configure.zcml`` in the
+   ``/path/to/instance/etc/package-includes`` directory.  The file
+   should only contain this::
+
+       <include package="quintagroup.plonetabs" />
+
+
+Alternatively, if you are using zc.buildout and the plone.recipe.zope2instance
+recipe to manage your project, you can do this:
+
+ * Add ``quintagroup.plonetabs`` to the list of eggs to install, e.g.:
+ 
+    [buildout]
+    ...
+    eggs =
+        ...
+        quintagroup.plonetabs
+        
+  * Tell the plone.recipe.zope2instance recipe to install a ZCML slug:
+  
+    [instance]
+    recipe = plone.recipe.zope2instance
+    ...
+    zcml =
+        quintagroup.plonetabs
+        
+  * Re-run buildout, e.g. with:
+  
+    $ ./bin/buildout
+        
+You can skip the ZCML slug if you are going to explicitly include the package
+from another package's configure.zcml file.
Index: /quintagroup.plonetabs/trunk/docs/LICENSE.GPL
===================================================================
--- /quintagroup.plonetabs/trunk/docs/LICENSE.GPL	(revision 540)
+++ /quintagroup.plonetabs/trunk/docs/LICENSE.GPL	(revision 540)
@@ -0,0 +1,225 @@
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
Index: /quintagroup.plonetabs/trunk/docs/LICENSE.txt
===================================================================
--- /quintagroup.plonetabs/trunk/docs/LICENSE.txt	(revision 540)
+++ /quintagroup.plonetabs/trunk/docs/LICENSE.txt	(revision 540)
@@ -0,0 +1,16 @@
+  quintagroup.plonetabs is copyright (c) "Quintagroup": http://quintagroup.com/ , 2008
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, 
+  MA 02111-1307 USA.
Index: /quintagroup.plonetabs/trunk/quintagroup/__init__.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/__init__.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/__init__.py	(revision 540)
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/__init__.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/__init__.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/__init__.py	(revision 540)
@@ -0,0 +1,2 @@
+def initialize(context):
+    pass
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/__init__.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/__init__.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/__init__.py	(revision 540)
@@ -0,0 +1,1 @@
+# I'm a package
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/configure.zcml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/configure.zcml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/configure.zcml	(revision 540)
@@ -0,0 +1,24 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser">
+    
+    <include file="kssactions.zcml" />
+    
+    <include file="resources.zcml" />
+    
+    <browser:page
+        name="plonetabs-controlpanel"
+        for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+        class=".plonetabs.PloneTabsControlPanel"
+        permission="cmf.ManagePortal"
+        allowed_interface = ".interfaces.IPloneTabsControlPanel"
+        />
+
+    <browser:page
+        name="plonetabs-header-macro"
+        for="*"
+        template="templates/plonetabs_header_macro.pt"
+        permission="zope.Public"
+        />
+    
+</configure>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/interfaces.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/interfaces.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/interfaces.py	(revision 540)
@@ -0,0 +1,44 @@
+from zope.interface import Interface
+
+class IPloneTabsControlPanel(Interface):
+    """ Configlet for managing portal_actions, quintagroup.plonetabs """
+    
+    def getPageTitle(category='portal_tabs'):
+        """ Return Title for configlet page for given category """
+    
+    def hasActions(category="portal_tabs"):
+        """ Whether there are actions in portal_actions with given category """
+    
+    def getPortalActions(category="portal_tabs"):
+        """ Return portal actions with given category """
+    
+    def isGeneratedTabs():
+        """ Whether disable_folder_section field is turned off """
+    
+    def isNotFoldersGenerated():
+        """ Whether disable_nonfolderish_sections field is turned off """
+    
+    def getActionsList(category="portal_tabs"):
+        """ Return html code for actions list with given category """
+    
+    def getAutoGenereatedSection(cat_name, errors):
+        """ Return html code for all autogenerated section """
+    
+    def getGeneratedTabs():
+        """ Return html code for autogenerated tabs """
+    
+    def getRootTabs():
+        """ Return portal root elements """
+    
+    def getCategories():
+        """ Return list of categories contained in portal_actions tool """
+    
+    def portal_tabs():
+        """ See global-sections viewlet """
+    
+    def selected_portal_tab():
+        """ See global-sections viewlet """
+    
+    def test(condition, ifTrue, ifFalse):
+        """ Instead of test function in skins page templates """
+    
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/kssactions.zcml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/kssactions.zcml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/kssactions.zcml	(revision 540)
@@ -0,0 +1,92 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser">
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_toggleGeneratedTabs"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-toggleGeneratedTabs"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_toggleActionsVisibility"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-toggleActionsVisibility"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_toggleRootsVisibility"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-toggleRootsVisibility"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_deleteAction"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-deleteAction"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_addAction"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-addAction"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_showEditForm"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-showEditForm"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_hideEditForm"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-hideEditForm"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_hideAddForm"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-hideAddForm"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_editAction"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-editAction"
+        permission="cmf.ManagePortal"
+        />
+    
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_orderActions"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-orderActions"
+        permission="cmf.ManagePortal"
+        />
+    <browser:page
+        for="plone.app.kss.interfaces.IPortalObject"
+        attribute="kss_changeCategory"
+        class=".plonetabs.PloneTabsControlPanel"
+        name="plonetabs-changeCategory"
+        permission="cmf.ManagePortal"
+        />
+    
+</configure>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/plonetabs.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/plonetabs.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/plonetabs.py	(revision 540)
@@ -0,0 +1,958 @@
+import urllib
+import re
+
+from Acquisition import aq_inner
+
+from zope.interface import implements
+from zope.component import getMultiAdapter
+from zope.app.container.interfaces import INameChooser
+from zope.viewlet.interfaces import IViewletManager, IViewlet
+
+from plone.app.layout.navigation.root import getNavigationRoot
+from plone.app.kss.plonekssview import PloneKSSView
+from kss.core import kssaction, KSSExplicitError
+
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.interfaces import IAction, IActionCategory
+from Products.CMFCore.ActionInformation import Action, ActionCategory
+from Products.CMFCore.Expression import Expression
+from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFPlone import utils
+from Products.CMFPlone.browser.navigation import get_view_url
+from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
+from Products.statusmessages.interfaces import IStatusMessage
+
+from quintagroup.plonetabs.config import *
+from interfaces import IPloneTabsControlPanel
+
+ACTION_ATTRS = ["id", "title", "url_expr", "available_expr", "visible"]
+UI_ATTRS = {"id": "id",
+            "title": "name",
+            "url_expr": "action",
+            "available_expr": "condition",
+            "visible": "visible"}
+
+bad_id = re.compile(r'[^a-zA-Z0-9-_~,.$\(\)# @]').search
+
+class PloneTabsControlPanel(PloneKSSView):
+    
+    implements(IPloneTabsControlPanel)
+    
+    template = ViewPageTemplateFile("templates/plonetabs.pt")
+    actionslist_template = ViewPageTemplateFile("templates/actionslist.pt")
+    autogenerated_template = ViewPageTemplateFile("templates/autogenerated.pt")
+    autogenerated_list = ViewPageTemplateFile("templates/autogeneratedlist.pt")
+    
+    # custom templates used to update page sections
+    sections_template = ViewPageTemplateFile("templates/sections.pt")
+    
+    # configuration variables
+    prefix = "tabslist_"
+    sufix = ""
+    
+    def __call__(self):
+        """ Perform the update and redirect if necessary, or render the page """
+        postback = True
+        errors = {}
+        context = aq_inner(self.context)
+        
+        form = self.request.form
+        action = form.get("action", "")
+        submitted = form.get('form.submitted', False)
+        
+        # action handler def handler(self, form)
+        if submitted:
+            if form.has_key('add.add'):
+                postback = self.manage_addAction(form, errors)
+            elif form.has_key("edit.save"):
+                postback = self.manage_editAction(form, errors)
+            elif form.has_key("edit.delete"):
+                postback = self.manage_deleteAction(form, errors)
+            elif form.has_key("edit.moveup"):
+                postback = self.manage_moveUpAction(form, errors)
+            elif form.has_key("edit.movedown"):
+                postback = self.manage_moveDownAction(form, errors)
+            elif form.has_key("autogenerated.save"):
+                postback = self.manage_setAutogeneration(form, errors)
+            else:
+                postback = True
+        
+        if postback:
+            return self.template(errors=errors)
+    
+    ########################################
+    # Methods for processing configlet posts
+    ########################################
+    
+    def manage_setAutogeneration(self, form, errors):
+        """ Process managing autogeneration settings """
+        
+        # set excludeFromNav property for root objects
+        portal = getMultiAdapter((aq_inner(self.context), self.request), name='plone_portal_state').portal()
+        generated_tabs = form.get("generated_tabs", '0')
+        nonfolderish_tabs = form.get("nonfolderish_tabs", '0')
+        
+        for item in self.getRootTabs():
+            obj = getattr(portal, item['id'], None)
+            if obj is not None:
+                checked = form.get(item['id'], None)
+                if checked == '1':
+                    obj.update(excludeFromNav=False)
+                else:
+                    obj.update(excludeFromNav=True)
+
+        # set disable_folder_sections property
+        if int(generated_tabs) == 1:
+            self.setSiteProperties(disable_folder_sections=False)
+        else:
+            self.setSiteProperties(disable_folder_sections=True)
+        
+        # set disable_nonfolderish_sections property
+        if int(nonfolderish_tabs) == 1:
+            self.setSiteProperties(disable_nonfolderish_sections=False)
+        else:
+            self.setSiteProperties(disable_nonfolderish_sections=True)
+        
+        # after successfull form processing make redirect with good message
+        IStatusMessage(self.request).addStatusMessage(_(u"Changes saved!"), type="info")
+        self.redirect()
+        return False
+    
+    def manage_addAction(self, form, errs):
+        """ Manage method to add a new action to given category,
+            if category doesn't exist, create it """
+        # extract posted data
+        id, cat_name, data = self.parseAddForm(form)
+        
+        # validate posted data
+        errors = self.validateActionFields(cat_name, data)
+        
+        # if not errors find (or create) category and set action to it
+        if not errors:
+            action = self.addAction(cat_name, data)
+            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action successfully added." % action.id), type="info")
+            self.redirect(search="category=%s" % cat_name)
+            return False
+        else:
+            errs.update(errors)
+            IStatusMessage(self.request).addStatusMessage(_(u"Please correct the indicated errors."), type="error")
+            return True
+    
+    def manage_editAction(self, form, errs):
+        """ Manage Method to update action """
+        # extract posted data
+        id, cat_name, data = self.parseEditForm(form)
+        
+        # get category and action to edit
+        category = self.getActionCategory(cat_name)
+        action = category[id]
+        
+        # validate posted data
+        errors = self.validateActionFields(cat_name, data, allow_dup=(id == data['id']))
+        
+        if not errors:
+            action = self.updateAction(id, cat_name, data)
+            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action saved." % action.id), type="info")
+            self.redirect(search="category=%s" % cat_name)
+            return False
+        else:
+            errs.update(self.processErrors(errors, sufix='_%s' % id)) # add edit form sufix to error ids
+            IStatusMessage(self.request).addStatusMessage(_(u"Please correct the indicated errors."), type="error")
+            return True
+    
+    def manage_deleteAction(self, form, errs):
+        """ Manage Method to delete action """
+        # extract posted data
+        id, cat_name, data = self.parseEditForm(form)
+        
+        # get category and action to delete
+        category = self.getActionCategory(cat_name)
+        if id in category.objectIds():
+            self.deleteAction(id, cat_name)
+            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action deleted." % id), type="info")
+            self.redirect(search="category=%s" % cat_name)
+            return False
+        else:
+            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
+            return True
+    
+    def manage_moveUpAction(self, form, errs):
+        """ Manage Method for moving up given action by one position """
+        # extract posted data
+        id, cat_name, data = self.parseEditForm(form)
+        
+        # get category and action to move
+        category = self.getActionCategory(cat_name)
+        if id in category.objectIds():
+            self.moveAction(id, cat_name, steps=1)
+            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action moved up." % id), type="info")
+            self.redirect(search="category=%s" % cat_name)
+            return False
+        else:
+            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
+            return True
+    
+    def manage_moveDownAction(self, form, errs):
+        """ Manage Method for moving down given action by one position """
+        # extract posted data
+        id, cat_name, data = self.parseEditForm(form)
+        
+        # get category and action to move
+        category = self.getActionCategory(cat_name)
+        if id in category.objectIds():
+            self.moveAction(id, cat_name, steps=-1)
+            IStatusMessage(self.request).addStatusMessage(_(u"'%s' action moved down." % id), type="info")
+            self.redirect(search="category=%s" % cat_name)
+            return False
+        else:
+            IStatusMessage(self.request).addStatusMessage(_(u"No '%s' action in '%s' category." % (id, cat_name)), type="error")
+            return True
+    
+    def redirect(self, url="", search="", url_hash=""):
+        """ Redirect to @@plonetabs-controlpanel configlet """
+        portal_url =  getMultiAdapter((self.context, self.request), name=u"plone_portal_state").portal_url()
+        url = (url == "") and "%s/%s" % (portal_url, "@@plonetabs-controlpanel") or url
+        search = (search != "") and "?%s" % search or search
+        url_hash = (url_hash != "") and "#%s" % url_hash or url_hash
+        self.request.response.redirect("%s%s%s" % (url, search, url_hash))
+    
+    ###################################
+    #
+    #  Methods - providers for templates
+    #
+    ###################################
+    
+    def getPageTitle(self, category="portal_tabs"):
+        """ See interface """
+        portal_props = getToolByName(self.context, "portal_properties")
+        default_title = "Plone '%s' Configuration" % category
+        
+        if not hasattr(portal_props, PROPERTY_SHEET):
+            return default_title
+        
+        sheet = getattr(portal_props, PROPERTY_SHEET)
+        if not hasattr(sheet, FIELD_NAME):
+            return default_title
+        
+        field = sheet.getProperty(FIELD_NAME)
+        dict = {}
+        for line in field:
+            cat, title = line.split("|", 2)
+            dict[cat] = title
+        
+        return dict.get(category, None) or default_title
+    
+    def hasActions(self, category="portal_tabs"):
+        """ See interface """
+        return len(getToolByName(self.context, "portal_actions").listActions(categories=[category,])) > 0
+    
+    def getPortalActions(self, category="portal_tabs"):
+        """ See interface """
+        portal_actions = getToolByName(self.context, "portal_actions")
+        
+        if category not in portal_actions.objectIds():
+            return []
+        
+        actions = []
+        for item in portal_actions[category].objectValues():
+            if IAction.providedBy(item):
+                actions.append(item)
+        
+        return actions
+    
+    def isGeneratedTabs(self):
+        """ See interface """
+        site_properties = getToolByName(self.context, "portal_properties").site_properties
+        return not site_properties.getProperty("disable_folder_sections", False)
+    
+    def isNotFoldersGenerated(self):
+        """ See interface """
+        site_properties = getToolByName(self.context, "portal_properties").site_properties
+        return not site_properties.getProperty("disable_nonfolderish_sections", False)
+    
+    def getActionsList(self, category="portal_tabs", errors={}, tabs=[]):
+        """ See interface """
+        kw = {'category': category, 'errors': errors}
+        if tabs:
+            kw['tabs'] = tabs
+        return self.actionslist_template(**kw)
+    
+    def getAutoGenereatedSection(self, cat_name, errors={}):
+        """ See interface """
+        return self.autogenerated_template(category=cat_name, errors=errors)
+    
+    def getGeneratedTabs(self):
+        """ See interface """
+        return self.autogenerated_list()
+    
+    def getRootTabs(self):
+        """ See interface """
+        context = aq_inner(self.context)
+        
+        portal_catalog = getToolByName(context, 'portal_catalog')
+        portal_properties = getToolByName(context, 'portal_properties')
+        navtree_properties = getattr(portal_properties, 'navtree_properties')
+        
+        # Build result dict
+        result = []
+        
+        # check whether tabs autogeneration is turned on
+        if not self.isGeneratedTabs():
+            return result
+        
+        query = {}
+        rootPath = getNavigationRoot(context)
+        query['path'] = {'query' : rootPath, 'depth' : 1}
+        query['portal_type'] = utils.typesToList(context)
+        
+        sortAttribute = navtree_properties.getProperty('sortAttribute', None)
+        if sortAttribute is not None:
+            query['sort_on'] = sortAttribute
+            
+            sortOrder = navtree_properties.getProperty('sortOrder', None)
+            if sortOrder is not None:
+                query['sort_order'] = sortOrder
+        
+        if navtree_properties.getProperty('enable_wf_state_filtering', False):
+            query['review_state'] = navtree_properties.getProperty('wf_states_to_show', [])
+        
+        query['is_default_page'] = False
+
+        if not self.isNotFoldersGenerated():
+            query['is_folderish'] = True
+
+        # Get ids not to list and make a dict to make the search fast
+        idsNotToList = navtree_properties.getProperty('idsNotToList', ())
+        excludedIds = {}
+        for id in idsNotToList:
+            excludedIds[id]=1
+
+        rawresult = portal_catalog.searchResults(**query)
+
+        # now add the content to results
+        for item in rawresult:
+            if not excludedIds.has_key(item.getId):
+                id, item_url = get_view_url(item)
+                data = {'name'       : utils.pretty_title_or_id(context, item),
+                        'id'         : id,
+                        'url'        : item_url,
+                        'description': item.Description,
+                        'exclude_from_nav' : item.exclude_from_nav}
+                result.append(data)
+        
+        return result
+    
+    def getCategories(self):
+        """ See interface """
+        portal_actions = getToolByName(self.context, "portal_actions")
+        return portal_actions.objectIds()
+    
+    #
+    # Methods to make this class looks like global sections viewlet
+    #
+    
+    def test(self, condition, ifTrue, ifFalse):
+        """ See interface """
+        if condition:
+            return ifTrue
+        else:
+            return ifFalse
+    
+    # methods for rendering global-sections viewlet via kss,
+    # due to bug in macroContent when global-section list is empty,
+    # ul have condition
+    def portal_tabs(self):
+        """ See global-sections viewlet """
+        actions = context_state = getMultiAdapter((self.context, self.request), name=u"plone_context_state").actions()
+        portal_tabs_view = getMultiAdapter((self.context, self.request), name="portal_tabs_view")
+        
+        return portal_tabs_view.topLevelTabs(actions=actions)
+    
+    def selected_portal_tab(self):
+        """ See global-sections viewlet """
+        selectedTabs = self.context.restrictedTraverse('selectedTabs')
+        selected_tabs = selectedTabs('index_html', self.context, self.portal_tabs())
+        
+        return selected_tabs['portal']
+    
+    ##########################
+    #
+    # KSS Server Actions
+    #
+    ##########################
+    
+    @kssaction
+    def kss_changeCategory(self, cat_name):
+        """ Change action category to manage """
+        ksscore = self.getCommandSet('core')
+        
+        # update actions list
+        actionslist = self.getActionsList(category=cat_name)
+        ksscore.replaceInnerHTML(ksscore.getHtmlIdSelector('tabslist'), actionslist)
+        
+        # update autogenerated sections
+        section = self.getAutoGenereatedSection(cat_name)
+        ksscore.replaceHTML(ksscore.getHtmlIdSelector('autogeneration_section'), section)
+        # and title
+        ksscore.replaceInnerHTML(ksscore.getHtmlIdSelector('plonetabs-form-title'),
+                                 self.getPageTitle(cat_name))
+        
+        # update category hidden field on adding form
+        ksscore.setAttribute(ksscore.getCssSelector('#addaction input[name=category]'), 'value', cat_name)
+        
+        # update state variable 'plonetabs-category' on client 
+        ksscore.setStateVar('plonetabs-category', cat_name)
+        
+        # hide portal status message
+        self.kss_hideStatusMessage(ksscore)
+        
+        # hide adding form
+        self.kss_hideAddForm()
+    
+    @kssaction
+    def kss_toggleGeneratedTabs(self, field, checked='0'):
+        """ Toggle autogenaration setting on configlet """
+        if checked == '1':
+            self.setSiteProperties(**{field: False})
+        else:
+            self.setSiteProperties(**{field: True})
+        
+        # update client
+        ksscore = self.getCommandSet("core")
+        content = self.getGeneratedTabs()
+        ksscore.replaceInnerHTML(ksscore.getHtmlIdSelector('roottabs'), content)
+        
+        # update global-sections viewlet
+        self.updatePortalTabsPageSection()
+    
+    @kssaction
+    def kss_toggleRootsVisibility(self, id, checked='0'):
+        """ Toggle visibility for portal root objects (exclude_from_nav) """
+        portal = getMultiAdapter((aq_inner(self.context), self.request), name='plone_portal_state').portal()
+        
+        # remove prefix, added for making ids on configlet unique ("roottabs_")
+        obj_id = id[len("roottabs_"):]
+        
+        if obj_id not in portal.objectIds():
+            raise KSSExplicitError, "Object with %s id doesn't exist in portal root" % obj_id
+        
+        if checked == '1':
+            checked = True
+        else:
+            checked = False
+        
+        portal[obj_id].update(excludeFromNav=not checked)
+        
+        # update client
+        ksscore = self.getCommandSet("core")
+        if checked:
+            ksscore.removeClass(ksscore.getHtmlIdSelector(id), value="invisible")
+        else:
+            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
+        
+        # update global-sections viewlet
+        self.updatePortalTabsPageSection()
+    
+    @kssaction
+    def kss_toggleActionsVisibility(self, id, checked='0', cat_name=None):
+        """ Toggle visibility for portal actions """
+        # validate input
+        act_id, category, action = self.kss_validateAction(id, cat_name)
+        self.updateAction(act_id, cat_name, {'id': act_id, 'visible': (checked == '1') or False})
+        
+        # update client
+        ksscore = self.getCommandSet("core")
+        if checked == '1':
+            ksscore.removeClass(ksscore.getHtmlIdSelector(id), value="invisible")
+        else:
+            ksscore.addClass(ksscore.getHtmlIdSelector(id), value="invisible")
+        self.updatePage(cat_name)
+    
+    @kssaction
+    def kss_deleteAction(self, id, cat_name):
+        """ Delete portal action with given id & category """
+        # validate input
+        act_id, category, action = self.kss_validateAction(id, cat_name)
+        self.deleteAction(act_id, cat_name)
+        
+        # update client
+        ksscore = self.getCommandSet("core")
+        # XXX TODO: fade effect during removing, to do this we need kukit js action/command plugin
+        ksscore.deleteNode(ksscore.getHtmlIdSelector(id))
+        
+        # issue portal message
+        self.getCommandSet('plone').issuePortalMessage(_(u"'%s' action successfully deleted." % act_id), msgtype="info")
+        
+        # update different sections of page depending on actions category
+        self.updatePage(cat_name)
+    
+    @kssaction
+    def kss_addAction(self):
+        """ KSS method to add new portal action """
+        # extract posted data
+        id, cat_name, data = self.parseAddForm(self.request.form)
+        
+        # validate posted data
+        errors = self.validateActionFields(cat_name, data)
+        
+        # if not errors find (or create) category and set action to it
+        ksscore = self.getCommandSet('core')
+        kssplone = self.getCommandSet('plone')
+        if not errors:
+            action = self.addAction(cat_name, data)
+            
+            # update client
+            # add one more action to actions list
+            content = self.getActionsList(category=cat_name, tabs=[action,])
+            ksscore.insertHTMLAsLastChild(ksscore.getHtmlIdSelector('tabslist'), content)
+            
+            # update reorder controls
+            #self.kss_checkReorderControls(cat_name)
+            
+            # hide adding form
+            ksscore.removeClass(ksscore.getHtmlIdSelector('addaction'), 'adding')
+            self.kss_toggleCollapsible(ksscore.getCssSelector('#addaction .headerAdvanced'), collapse='true')
+            
+            # set client state var 'plonetabs-addingTitle' to empty string for correct id autogeneration functionality
+            ksscore.setStateVar('plonetabs-addingTitle', '')
+            
+            # reset adding form
+            self.kss_resetForm(ksscore.getHtmlIdSelector('addaction'))
+            
+            # remove focus from name input
+            self.kss_blur(ksscore.getHtmlIdSelector('actname'))
+            
+            # issue portal message
+            kssplone.issuePortalMessage(_(u"'%s' action successfully added." % action.id), msgtype="info")
+            
+            # update page
+            self.updatePage(cat_name)
+        else:
+            # expand advanced section if there are errors in id or condition
+            if errors.has_key('id') or errors.has_key('available_expr'):
+                self.kss_toggleCollapsible(ksscore.getCssSelector('#addaction .headerAdvanced'), collapse='false')
+            
+            # send error message
+            kssplone.issuePortalMessage(_(u"Please correct the indicated errors."), msgtype="error")
+        
+        # update errors on client form
+        self.kss_issueErrors(errors)
+    
+    @kssaction
+    def kss_hideAddForm(self):
+        """"Hide Add form, reset it and remove error messages"""
+        # no server changes, only update client
+        ksscore = self.getCommandSet("core")
+        # hide form itself
+        ksscore.removeClass(ksscore.getHtmlIdSelector('addaction'), 'adding')
+        # collapse advanced section
+        self.kss_toggleCollapsible(ksscore.getCssSelector('#addaction .headerAdvanced'), collapse='true')
+        # reset form inputs
+        self.kss_resetForm(ksscore.getHtmlIdSelector('addaction'))
+        # remove form errors if such exist
+        self.kss_issueErrors({})
+    
+    @kssaction
+    def kss_showEditForm(self, id, cat_name):
+        """Show edit form for given action"""
+        act_id, category, action = self.kss_validateAction(id, cat_name)
+        
+        # fetch data
+        action_info = self.copyAction(action)
+        action_info["editing"] = True
+        
+        # update client
+        ksscore = self.getCommandSet("core")
+        content = self.getActionsList(category=cat_name, tabs=[action_info,])
+        ksscore.replaceHTML(ksscore.getHtmlIdSelector(id), content)
+        
+        # focus name field
+        ksscore.focus(ksscore.getCssSelector("#%s input[name=title_%s]" % (id, act_id)))
+    
+    @kssaction
+    def kss_hideEditForm(self, id, cat_name):
+        """Hide edit form for given action"""
+        act_id, category, action = self.kss_validateAction(id, cat_name)
+        
+        # update client
+        ksscore = self.getCommandSet("core")
+        content = self.getActionsList(category=cat_name, tabs=[action,])
+        ksscore.replaceHTML(ksscore.getHtmlIdSelector(id), content)
+    
+    @kssaction
+    def kss_editAction(self):
+        """ Update action's properties """
+        id, cat_name, data = self.parseEditForm(self.request.form)
+        
+        # get category and action to edit
+        category = self.getActionCategory(cat_name)
+        action = category[id]
+        
+        # validate posted data
+        errors = self.validateActionFields(cat_name, data, allow_dup=(id == data['id']))
+        
+        html_id = '%s%s%s' % (self.prefix, id, self.sufix)
+        ksscore = self.getCommandSet('core')
+        kssplone = self.getCommandSet('plone')
+        if not errors:
+            action = self.updateAction(id, cat_name, data)
+            
+            # update client
+            # replace action item with updated one
+            content = self.getActionsList(category=cat_name, tabs=[action,])
+            ksscore.replaceHTML(ksscore.getHtmlIdSelector(html_id), content)
+            
+            # issue portal message
+            kssplone.issuePortalMessage(_(u"'%s' action saved." % action.id), msgtype="info")
+            
+            # update page
+            self.updatePage(cat_name)
+        else:
+            # issue error messages
+            self.kss_issueErrors(errors, editform=id)
+            
+            # expand advanced section if there are errors in id, action url or condition
+            if errors.has_key('id') or errors.has_key('available_expr') or errors.has_key('url_expr'):
+                self.kss_toggleCollapsible(ksscore.getCssSelector('#%s .headerAdvanced' % html_id), collapse='false')
+            
+            # send error message
+            kssplone.issuePortalMessage(_(u"Please correct the indicated errors."), msgtype="error")
+    
+    @kssaction
+    def kss_orderActions(self):
+        """ Update actions order in the given category """
+        form = self.request.form
+        cat_name = form['cat_name']
+        category = self.getActionCategory(cat_name)
+        
+        # decode URI components and collect ids from request
+        components = urllib.unquote(form['actions']).split('&')
+        if self.sufix == '':
+            ids = [component[len(self.prefix):] for component in components]
+        else:
+            ids = [component[len(self.prefix):-len(self.sufix)] for component in components]
+        
+        # do actual sorting
+        category.moveObjectsByDelta(ids, -len(category.objectIds()))
+        
+        # update client
+        self.updatePage(cat_name)
+    
+    #
+    # Utility Methods
+    #
+    
+    def fixExpression(self, expr):
+        """Fix expression appropriatly for tal format"""
+        if expr.find('/') == 0:
+            return 'string:${portal_url}%s' % expr
+        elif re.compile('^(ht|f)tps?\:', re.I).search(expr):
+            return 'string:%s' % expr
+        #elif re.compile('^(python:|string:|not:|exists:|nocall:|path:)', re.I).search(expr):
+            #return expr
+        elif expr.find(':') != -1:
+            return expr
+        else:
+            return 'string:${object_url}/%s' % expr
+    
+    def copyAction(self, action):
+        """ Copy action to dictionary """
+        action_info = {'description':action.description}
+        for attr in ACTION_ATTRS:
+            action_info[attr] = getattr(action, attr)
+        return action_info
+    
+    def validateActionFields(self, cat_name, data, allow_dup=False):
+        """ Check action fields on validity """
+        errors = {}
+        
+        if allow_dup:
+            category = ActionCategory(cat_name)           # create dummy category to avoid id duplication during action update
+        else:
+            category = self.getOrCreateCategory(cat_name) # get or create (if necessary) actions category
+        
+        # validate action id
+        chooser = INameChooser(category)
+        try:
+            chooser.checkName(data['id'], self.context)
+        except Exception, e:
+            errors['id'] = "%s" % str(e)
+        
+        # validate action name
+        if not data['title'].strip():
+            errors['title'] = 'Empty or invalid title specified'
+        
+        # validate condition expression
+        if data['available_expr']:
+            try:
+                Expression(data['available_expr'])
+            except Exception, e:
+                errors["available_expr"] = "%s" % str(e)
+        
+        # validate action expression
+        if data['url_expr']:
+            try:
+                Expression(data['url_expr'])
+            except Exception, e:
+                errors["url_expr"] = "%s" % str(e)
+        
+        return errors
+    
+    def processErrors(self, errors, prefix='', sufix=''):
+        """ Add prefixes, sufixes to error ids 
+            This is necessary during edit form validation,
+            because every edit form on the page has it's own sufix (id) """
+        if not (prefix or sufix):
+            return errors
+        
+        result = {}
+        for key, value in errors.items():
+            result['%s%s%s' % (prefix, key, sufix)] = value
+        
+        return result
+    
+    def parseEditForm(self, form):
+        """ Extract all needed fields from edit form """
+        # get original id and category
+        info = {}
+        id = form['orig_id']
+        category = form['category']
+        
+        # preprocess 'visible' field (checkbox needs special checking)
+        if form.has_key('visible_%s' % id):
+            form['visible_%s' % id] = True
+        else:
+            form['visible_%s' % id] = False
+        
+        # collect all action fields
+        for attr in ACTION_ATTRS:
+            info[attr] = form['%s_%s' % (attr, id)]
+        
+        return (id, category, info)
+    
+    def parseAddForm(self, form):
+        """ Extract all needed fields from add form """
+        info = {}
+        id = form['id']
+        category = form['category']
+        
+        # preprocess 'visible' field (checkbox needs special checking)
+        if form.has_key('visible') and form['visible']:
+            form['visible'] = True
+        else:
+            form['visible'] = False
+        
+        # fix expression fields
+        form['url_expr'] = self.fixExpression(form['url_expr'])
+        
+        # collect all action fields
+        for attr in ACTION_ATTRS:
+            info[attr] = form[attr]
+        
+        return (id, category, info)
+    
+    def getActionCategory(self, name):
+        portal_actions = getToolByName(self.context, 'portal_actions')
+        return portal_actions[name]
+    
+    def getOrCreateCategory(self, name):
+        """ Get or create (if necessary) category """
+        portal_actions = getToolByName(self.context, 'portal_actions')
+        if name not in map(lambda x: x.id, filter(lambda x: IActionCategory.providedBy(x), portal_actions.objectValues())):
+            portal_actions._setObject(name, ActionCategory(name))
+        return self.getActionCategory(name)
+    
+    def setSiteProperties(self, **kw):
+        """ Change site_properties """
+        site_properties = getToolByName(self.context, "portal_properties").site_properties
+        site_properties.manage_changeProperties(**kw)
+        return True
+    
+    #
+    # Utility methods for the kss actions management
+    #
+    
+    def kss_hideStatusMessage(self, ksscore):
+        """Hide Portal Status Message"""
+        ksscore.setStyle(ksscore.getHtmlIdSelector('kssPortalMessage'), 'display', 'none')
+    
+    def kss_validateAction(self, id, cat_name):
+        """ Check whether action with given id exists in cat_name category """
+        try:
+            category = self.getActionCategory(cat_name)
+        except Exception:
+            raise KSSExplicitError, "'%s' action category does not exist" % cat_name
+        
+        # extract action id from given list item id on client
+        action_id = self.sufix and id[len(self.prefix):-len(self.sufix)] or id[len(self.prefix):]
+        try:
+            action = category[action_id]
+        except Exception:
+            raise KSSExplicitError, "No '%s' action in '%s' category." % (action_id, cat_name)
+        
+        return (action_id, category, action)
+    
+    def kss_issueErrors(self, errors, editform=False, fields=ACTION_ATTRS):
+        """ Display error messages on the client """
+        ksscore = self.getCommandSet('core')
+        for field in fields:
+            self.kss_issueFieldError(ksscore, field, errors.get(field, False), editform)
+    
+    def kss_issueFieldError(self, ksscore, name, error, editform):
+        """ Issue this error message for the field """
+        if editform:
+            id = '%s%s%s' % (self.prefix, editform, self.sufix)
+            field_selector = ksscore.getCssSelector('#%s .edit-field-%s' % (id, UI_ATTRS.get(name, name)))
+            field_error_selector = ksscore.getCssSelector('#%s .edit-field-%s .error-container' % (id, UI_ATTRS.get(name, name)))
+        else:
+            field_selector = ksscore.getCssSelector('#addaction .field-%s' % UI_ATTRS.get(name, name))
+            field_error_selector = ksscore.getCssSelector('#addaction .field-%s .error-container' % UI_ATTRS.get(name, name))
+
+        if error:
+            ksscore.replaceInnerHTML(field_error_selector, _(error))
+            ksscore.addClass(field_selector, 'error')
+        else:
+            ksscore.clearChildNodes(field_error_selector)
+            ksscore.removeClass(field_selector, 'error')
+    
+    def kss_toggleCollapsible(self, selector, collapsed=None, expanded=None, collapse=None):
+        """ KSS Server command to add plonetabs-toggleCollapsible client action to response """
+        command = self.commands.addCommand('plonetabs-toggleCollapsible', selector)
+        if collapsed is not None:
+            data = command.addParam('collapsed', collapsed)
+        if expanded is not None:
+            data = command.addParam('expanded', expanded)
+        if collapse is not None:
+            data = command.addParam('collapse', collapse)
+    
+    def kss_resetForm(self, selector):
+        """ KSS Server command to reset form on client """
+        command = self.commands.addCommand('plonetabs-resetForm', selector)
+    
+    def kss_blur(self, selector):
+        """ KSS Server command to remove focus from input """
+        command = self.commands.addCommand('plonetabs-blur', selector)
+    
+    def kss_replaceOrInsert(self, selector, parentSelector, html, withKssSetup='True', alternativeHTML='', selectorType='',
+                                  position='', positionSelector='', positionSelectorType=''):
+        """ KSS Server command to execute replaceOrInsert client action """
+        command = self.commands.addCommand('plonetabs-replaceOrInsert', selector)
+        data = command.addParam('selector', parentSelector)
+        data = command.addHtmlParam('html', html)
+        data = command.addParam('withKssSetup', withKssSetup)
+        if alternativeHTML:
+            data = command.addHtmlParam('alternativeHTML', alternativeHTML)
+        if selectorType:
+            data = command.addParam('selectorType', selectorType)
+        if position:
+            data = command.addParam('position', position)
+        if positionSelector:
+            data = command.addParam('positionSelector', positionSelector)
+        if positionSelectorType:
+            data = command.addParam('positionSelectorType', positionSelectorType)
+    
+    def renderViewlet(self, manager, name):
+        if isinstance(manager, basestring):
+            manager = getMultiAdapter((self.context, self.request, self,), IViewletManager, name=manager)
+        renderer = getMultiAdapter((self.context, self.request, self, manager), IViewlet, name=name)
+        renderer = renderer.__of__(self.context)
+        renderer.update()
+        return renderer.render()
+    
+    #
+    # Basic API to work with portal actions tool in a more pleasent way
+    #
+    
+    def addAction(self, cat_name, data):
+        """ Create and add new action to category with given name """
+        id = data.pop('id')
+        category = self.getOrCreateCategory(cat_name)
+        action = Action(id, **data)
+        category._setObject(id, action)
+        return action
+    
+    def updateAction(self, id, cat_name, data):
+        """ Update action with given id and category """
+        new_id = data.pop('id')
+        category = self.getActionCategory(cat_name)
+        
+        # rename action if necessary
+        if id != new_id:
+            category.manage_renameObject(id, new_id)
+        
+        # get action
+        action = category[new_id]
+        
+        # update action properties
+        for attr in data.keys():
+            if data.has_key(attr):
+                action._setPropValue(attr, data[attr])
+        
+        return action
+    
+    def deleteAction(self, id, cat_name):
+        """ Delete action with given id from given category """
+        category = self.getActionCategory(cat_name)
+        category.manage_delObjects(ids=[id,])
+        return True
+    
+    def moveAction(self, id, cat_name, steps=0):
+        """ Move action by a given steps """
+        if steps != 0:
+            category = self.getActionCategory(cat_name)
+            if steps > 0:
+                category.moveObjectsUp([id,], steps)
+            else:
+                category.moveObjectsDown([id,], abs(steps))
+            return True
+        return False
+    
+    #
+    # KSS Methods that are used to update different parts of the page
+    # accordingly to category
+    #
+    
+    def updatePage(self, category):
+        """ Seek for according method in class and calls it if found
+            Example of making up method's name:
+                portal_tabs => updatePortalTabsPageSection """
+        method_name = 'update%sPageSection' % ''.join(map(lambda x: x.capitalize(), category.split('_')))
+        if hasattr(self, method_name):
+            getattr(self, method_name)()
+    
+    def updatePortalTabsPageSection(self):
+        """ Method for updating global-sections on client """
+        ksscore = self.getCommandSet("core")
+        self.kss_replaceOrInsert(ksscore.getHtmlIdSelector("portal-header"),
+                                 "portal-globalnav",
+                                 self.sections_template(),
+                                 withKssSetup='False',
+                                 selectorType='htmlid')
+    
+    def updateSiteActionsPageSection(self):
+        """ Method for updating site action on client """
+        ksscore = self.getCommandSet("core")
+        self.kss_replaceOrInsert(ksscore.getHtmlIdSelector("portal-header"),
+                                 "portal-siteactions",
+                                 self.renderViewlet("plone.portalheader", "plone.site_actions"),
+                                 withKssSetup='False',
+                                 selectorType='htmlid',
+                                 position="before",
+                                 positionSelector="portal-searchbox",
+                                 positionSelectorType="htmlid")
+        
+        #ksszope = self.getCommandSet("zope")
+        #ksszope.refreshViewlet(
+            #self.getCommandSet("core").getHtmlIdSelector("portal-siteactions"),
+            #"plone.portalheader",
+            #"plone.site_actions")
+    
+    def updateUserPageSection(self):
+        """ Method for updating site action on client """
+        ksszope = self.getCommandSet("zope")
+        ksszope.refreshViewlet(
+            self.getCommandSet("core").getHtmlIdSelector("portal-personaltools-wrapper"),
+            "plone.portaltop",
+            "plone.personal_bar")
+
+
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources.zcml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources.zcml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources.zcml	(revision 540)
@@ -0,0 +1,63 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser">
+    
+    <!-- Register images -->
+    
+    <browser:resource
+        name="plonetabs.gif"
+        image="./images/plonetabs.gif"
+        />
+    
+    <browser:resource
+        name="busy_icon.gif"
+        image="./images/busy_icon.gif"
+        />
+    
+    <browser:resource
+        name="trash.gif"
+        image="./images/trash.gif"
+        />
+    
+    <browser:resource
+        name="treeExpanded.gif"
+        image="./images/treeExpanded.gif"
+        />
+    
+    <browser:resource
+        name="treeCollapsed.gif"
+        image="./images/treeCollapsed.gif"
+        />
+    
+    <browser:resource
+        name="drag.gif"
+        image="./images/drag.gif"
+        />
+    
+    <!-- Register stylesheets -->
+    
+    <browser:resource
+        name="plonetabs.css"
+        file="./resources/plonetabs.css"
+        />
+    
+    <!-- Register kukit stylesheets -->
+    
+    <browser:resource
+        name="plonetabs.kss"
+        file="./resources/plonetabs.kss"
+        />
+    
+    <!-- Register scriptaculous dragdrop js library -->
+    
+    <browser:resource
+        name="sa_dragdrop.js"
+        file="./resources/dragdrop.js"
+        />
+
+    <browser:resource
+        name="pt_effects.js"
+        file="./resources/effects.js"
+        />
+    
+</configure>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/dragdrop.js
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/dragdrop.js	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/dragdrop.js	(revision 540)
@@ -0,0 +1,944 @@
+// script.aculo.us dragdrop.js v1.6.5, Wed Nov 08 14:17:49 CET 2006
+
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+// 
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+if(typeof Effect == 'undefined')
+  throw("dragdrop.js requires including script.aculo.us' effects.js library");
+
+var Droppables = {
+  drops: [],
+
+  remove: function(element) {
+    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+  },
+
+  add: function(element) {
+    element = $(element);
+    var options = Object.extend({
+      greedy:     true,
+      hoverclass: null,
+      tree:       false
+    }, arguments[1] || {});
+
+    // cache containers
+    if(options.containment) {
+      options._containers = [];
+      var containment = options.containment;
+      if((typeof containment == 'object') && 
+        (containment.constructor == Array)) {
+        containment.each( function(c) { options._containers.push($(c)) });
+      } else {
+        options._containers.push($(containment));
+      }
+    }
+    
+    if(options.accept) options.accept = [options.accept].flatten();
+
+    Element.makePositioned(element); // fix IE
+    options.element = element;
+
+    this.drops.push(options);
+  },
+  
+  findDeepestChild: function(drops) {
+    deepest = drops[0];
+      
+    for (i = 1; i < drops.length; ++i)
+      if (Element.isParent(drops[i].element, deepest.element))
+        deepest = drops[i];
+    
+    return deepest;
+  },
+
+  isContained: function(element, drop) {
+    var containmentNode;
+    if(drop.tree) {
+      containmentNode = element.treeNode; 
+    } else {
+      containmentNode = element.parentNode;
+    }
+    return drop._containers.detect(function(c) { return containmentNode == c });
+  },
+  
+  isAffected: function(point, element, drop) {
+    return (
+      (drop.element!=element) &&
+      ((!drop._containers) ||
+        this.isContained(element, drop)) &&
+      ((!drop.accept) ||
+        (Element.classNames(element).detect( 
+          function(v) { return drop.accept.include(v) } ) )) &&
+      Position.within(drop.element, point[0], point[1]) );
+  },
+
+  deactivate: function(drop) {
+    if(drop.hoverclass)
+      Element.removeClassName(drop.element, drop.hoverclass);
+    this.last_active = null;
+  },
+
+  activate: function(drop) {
+    if(drop.hoverclass)
+      Element.addClassName(drop.element, drop.hoverclass);
+    this.last_active = drop;
+  },
+
+  show: function(point, element) {
+    if(!this.drops.length) return;
+    var affected = [];
+    
+    if(this.last_active) this.deactivate(this.last_active);
+    this.drops.each( function(drop) {
+      if(Droppables.isAffected(point, element, drop))
+        affected.push(drop);
+    });
+        
+    if(affected.length>0) {
+      drop = Droppables.findDeepestChild(affected);
+      Position.within(drop.element, point[0], point[1]);
+      if(drop.onHover)
+        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+      
+      Droppables.activate(drop);
+    }
+  },
+
+  fire: function(event, element) {
+    if(!this.last_active) return;
+    Position.prepare();
+
+    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+      if (this.last_active.onDrop) 
+        this.last_active.onDrop(element, this.last_active.element, event);
+  },
+
+  reset: function() {
+    if(this.last_active)
+      this.deactivate(this.last_active);
+  }
+}
+
+var Draggables = {
+  drags: [],
+  observers: [],
+  
+  register: function(draggable) {
+    if(this.drags.length == 0) {
+      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+      
+      Event.observe(document, "mouseup", this.eventMouseUp);
+      Event.observe(document, "mousemove", this.eventMouseMove);
+      Event.observe(document, "keypress", this.eventKeypress);
+    }
+    this.drags.push(draggable);
+  },
+  
+  unregister: function(draggable) {
+    this.drags = this.drags.reject(function(d) { return d==draggable });
+    if(this.drags.length == 0) {
+      Event.stopObserving(document, "mouseup", this.eventMouseUp);
+      Event.stopObserving(document, "mousemove", this.eventMouseMove);
+      Event.stopObserving(document, "keypress", this.eventKeypress);
+    }
+  },
+  
+  activate: function(draggable) {
+    if(draggable.options.delay) { 
+      this._timeout = setTimeout(function() { 
+        Draggables._timeout = null; 
+        window.focus(); 
+        Draggables.activeDraggable = draggable; 
+      }.bind(this), draggable.options.delay); 
+    } else {
+      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+      this.activeDraggable = draggable;
+    }
+  },
+  
+  deactivate: function() {
+    this.activeDraggable = null;
+  },
+  
+  updateDrag: function(event) {
+    if(!this.activeDraggable) return;
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    // Mozilla-based browsers fire successive mousemove events with
+    // the same coordinates, prevent needless redrawing (moz bug?)
+    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+    this._lastPointer = pointer;
+    
+    this.activeDraggable.updateDrag(event, pointer);
+  },
+  
+  endDrag: function(event) {
+    if(this._timeout) { 
+      clearTimeout(this._timeout); 
+      this._timeout = null; 
+    }
+    if(!this.activeDraggable) return;
+    this._lastPointer = null;
+    this.activeDraggable.endDrag(event);
+    this.activeDraggable = null;
+  },
+  
+  keyPress: function(event) {
+    if(this.activeDraggable)
+      this.activeDraggable.keyPress(event);
+  },
+  
+  addObserver: function(observer) {
+    this.observers.push(observer);
+    this._cacheObserverCallbacks();
+  },
+  
+  removeObserver: function(element) {  // element instead of observer fixes mem leaks
+    this.observers = this.observers.reject( function(o) { return o.element==element });
+    this._cacheObserverCallbacks();
+  },
+  
+  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
+    if(this[eventName+'Count'] > 0)
+      this.observers.each( function(o) {
+        if(o[eventName]) o[eventName](eventName, draggable, event);
+      });
+    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
+  },
+  
+  _cacheObserverCallbacks: function() {
+    ['onStart','onEnd','onDrag'].each( function(eventName) {
+      Draggables[eventName+'Count'] = Draggables.observers.select(
+        function(o) { return o[eventName]; }
+      ).length;
+    });
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable._dragging    = {};
+
+Draggable.prototype = {
+  initialize: function(element) {
+    var defaults = {
+      handle: false,
+      reverteffect: function(element, top_offset, left_offset) {
+        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
+          queue: {scope:'_draggable', position:'end'}
+        });
+      },
+      endeffect: function(element) {
+        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
+        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
+          queue: {scope:'_draggable', position:'end'},
+          afterFinish: function(){ 
+            Draggable._dragging[element] = false 
+          }
+        }); 
+      },
+      zindex: 1000,
+      revert: false,
+      scroll: false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
+      delay: 0
+    };
+    
+    if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
+      Object.extend(defaults, {
+        starteffect: function(element) {
+          element._opacity = Element.getOpacity(element);
+          Draggable._dragging[element] = true;
+          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
+        }
+      });
+    
+    var options = Object.extend(defaults, arguments[1] || {});
+
+    this.element = $(element);
+    
+    if(options.handle && (typeof options.handle == 'string'))
+      this.handle = this.element.down('.'+options.handle, 0);
+    
+    if(!this.handle) this.handle = $(options.handle);
+    if(!this.handle) this.handle = this.element;
+    
+    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
+      options.scroll = $(options.scroll);
+      this._isScrollChild = Element.childOf(this.element, options.scroll);
+    }
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.delta    = this.currentDelta();
+    this.options  = options;
+    this.dragging = false;   
+
+    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+    Event.observe(this.handle, "mousedown", this.eventMouseDown);
+    
+    Draggables.register(this);
+  },
+  
+  destroy: function() {
+    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+    Draggables.unregister(this);
+  },
+  
+  currentDelta: function() {
+    return([
+      parseInt(Element.getStyle(this.element,'left') || '0'),
+      parseInt(Element.getStyle(this.element,'top') || '0')]);
+  },
+  
+  initDrag: function(event) {
+    if(typeof Draggable._dragging[this.element] != 'undefined' &&
+      Draggable._dragging[this.element]) return;
+    if(Event.isLeftClick(event)) {    
+      // abort on form elements, fixes a Firefox issue
+      var src = Event.element(event);
+      if(src.tagName && (
+        src.tagName=='INPUT' ||
+        src.tagName=='SELECT' ||
+        src.tagName=='OPTION' ||
+        src.tagName=='BUTTON' ||
+        src.tagName=='TEXTAREA')) return;
+        
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var pos     = Position.cumulativeOffset(this.element);
+      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+      
+      Draggables.activate(this);
+      Event.stop(event);
+    }
+  },
+  
+  startDrag: function(event) {
+    this.dragging = true;
+    
+    if(this.options.zindex) {
+      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+      this.element.style.zIndex = this.options.zindex;
+    }
+    
+    if(this.options.ghosting) {
+      this._clone = this.element.cloneNode(true);
+      Position.absolutize(this.element);
+      this.element.parentNode.insertBefore(this._clone, this.element);
+    }
+    
+    if(this.options.scroll) {
+      if (this.options.scroll == window) {
+        var where = this._getWindowScroll(this.options.scroll);
+        this.originalScrollLeft = where.left;
+        this.originalScrollTop = where.top;
+      } else {
+        this.originalScrollLeft = this.options.scroll.scrollLeft;
+        this.originalScrollTop = this.options.scroll.scrollTop;
+      }
+    }
+    
+    Draggables.notify('onStart', this, event);
+        
+    if(this.options.starteffect) this.options.starteffect(this.element);
+  },
+  
+  updateDrag: function(event, pointer) {
+    if(!this.dragging) this.startDrag(event);
+    Position.prepare();
+    Droppables.show(pointer, this.element);
+    Draggables.notify('onDrag', this, event);
+    
+    this.draw(pointer);
+    if(this.options.change) this.options.change(this);
+    
+    if(this.options.scroll) {
+      this.stopScrolling();
+      
+      var p;
+      if (this.options.scroll == window) {
+        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+      } else {
+        p = Position.page(this.options.scroll);
+        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
+        p[1] += this.options.scroll.scrollTop + Position.deltaY;
+        p.push(p[0]+this.options.scroll.offsetWidth);
+        p.push(p[1]+this.options.scroll.offsetHeight);
+      }
+      var speed = [0,0];
+      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+      this.startScrolling(speed);
+    }
+    
+    // fix AppleWebKit rendering
+    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+    
+    Event.stop(event);
+  },
+  
+  finishDrag: function(event, success) {
+    this.dragging = false;
+
+    if(this.options.ghosting) {
+      Position.relativize(this.element);
+      Element.remove(this._clone);
+      this._clone = null;
+    }
+
+    if(success) Droppables.fire(event, this.element);
+    Draggables.notify('onEnd', this, event);
+
+    var revert = this.options.revert;
+    if(revert && typeof revert == 'function') revert = revert(this.element);
+    
+    var d = this.currentDelta();
+    if(revert && this.options.reverteffect) {
+      this.options.reverteffect(this.element, 
+        d[1]-this.delta[1], d[0]-this.delta[0]);
+    } else {
+      this.delta = d;
+    }
+
+    if(this.options.zindex)
+      this.element.style.zIndex = this.originalZ;
+
+    if(this.options.endeffect) 
+      this.options.endeffect(this.element);
+      
+    Draggables.deactivate(this);
+    Droppables.reset();
+  },
+  
+  keyPress: function(event) {
+    if(event.keyCode!=Event.KEY_ESC) return;
+    this.finishDrag(event, false);
+    Event.stop(event);
+  },
+  
+  endDrag: function(event) {
+    if(!this.dragging) return;
+    this.stopScrolling();
+    this.finishDrag(event, true);
+    Event.stop(event);
+  },
+  
+  draw: function(point) {
+    var pos = Position.cumulativeOffset(this.element);
+    if(this.options.ghosting) {
+      var r   = Position.realOffset(this.element);
+      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+    }
+    
+    var d = this.currentDelta();
+    pos[0] -= d[0]; pos[1] -= d[1];
+    
+    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
+      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+    }
+    
+    var p = [0,1].map(function(i){ 
+      return (point[i]-pos[i]-this.offset[i]) 
+    }.bind(this));
+    
+    if(this.options.snap) {
+      if(typeof this.options.snap == 'function') {
+        p = this.options.snap(p[0],p[1],this);
+      } else {
+      if(this.options.snap instanceof Array) {
+        p = p.map( function(v, i) {
+          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+      } else {
+        p = p.map( function(v) {
+          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+      }
+    }}
+    
+    var style = this.element.style;
+    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+      style.left = p[0] + "px";
+    if((!this.options.constraint) || (this.options.constraint=='vertical'))
+      style.top  = p[1] + "px";
+    
+    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+  },
+  
+  stopScrolling: function() {
+    if(this.scrollInterval) {
+      clearInterval(this.scrollInterval);
+      this.scrollInterval = null;
+      Draggables._lastScrollPointer = null;
+    }
+  },
+  
+  startScrolling: function(speed) {
+    if(!(speed[0] || speed[1])) return;
+    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+    this.lastScrolled = new Date();
+    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+  },
+  
+  scroll: function() {
+    var current = new Date();
+    var delta = current - this.lastScrolled;
+    this.lastScrolled = current;
+    if(this.options.scroll == window) {
+      with (this._getWindowScroll(this.options.scroll)) {
+        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+          var d = delta / 1000;
+          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+        }
+      }
+    } else {
+      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
+    }
+    
+    Position.prepare();
+    Droppables.show(Draggables._lastPointer, this.element);
+    Draggables.notify('onDrag', this);
+    if (this._isScrollChild) {
+      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+      if (Draggables._lastScrollPointer[0] < 0)
+        Draggables._lastScrollPointer[0] = 0;
+      if (Draggables._lastScrollPointer[1] < 0)
+        Draggables._lastScrollPointer[1] = 0;
+      this.draw(Draggables._lastScrollPointer);
+    }
+    
+    if(this.options.change) this.options.change(this);
+  },
+  
+  _getWindowScroll: function(w) {
+    var T, L, W, H;
+    with (w.document) {
+      if (w.document.documentElement && documentElement.scrollTop) {
+        T = documentElement.scrollTop;
+        L = documentElement.scrollLeft;
+      } else if (w.document.body) {
+        T = body.scrollTop;
+        L = body.scrollLeft;
+      }
+      if (w.innerWidth) {
+        W = w.innerWidth;
+        H = w.innerHeight;
+      } else if (w.document.documentElement && documentElement.clientWidth) {
+        W = documentElement.clientWidth;
+        H = documentElement.clientHeight;
+      } else {
+        W = body.offsetWidth;
+        H = body.offsetHeight
+      }
+    }
+    return { top: T, left: L, width: W, height: H };
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+  initialize: function(element, observer) {
+    this.element   = $(element);
+    this.observer  = observer;
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  
+  onStart: function() {
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  
+  onEnd: function() {
+    Sortable.unmark();
+    if(this.lastValue != Sortable.serialize(this.element))
+      this.observer(this.element)
+  }
+}
+
+var Sortable = {
+  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
+  
+  sortables: {},
+  
+  _findRootElement: function(element) {
+    while (element.tagName != "BODY") {  
+      if(element.id && Sortable.sortables[element.id]) return element;
+      element = element.parentNode;
+    }
+  },
+
+  options: function(element) {
+    element = Sortable._findRootElement($(element));
+    if(!element) return;
+    return Sortable.sortables[element.id];
+  },
+  
+  destroy: function(element){
+    var s = Sortable.options(element);
+    
+    if(s) {
+      Draggables.removeObserver(s.element);
+      s.droppables.each(function(d){ Droppables.remove(d) });
+      s.draggables.invoke('destroy');
+      
+      delete Sortable.sortables[s.element.id];
+    }
+  },
+
+  create: function(element) {
+    element = $(element);
+    var options = Object.extend({ 
+      element:     element,
+      tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,
+      treeTag:     'ul',
+      overlap:     'vertical', // one of 'vertical', 'horizontal'
+      constraint:  'vertical', // one of 'vertical', 'horizontal', false
+      containment: element,    // also takes array of elements (or id's); or false
+      handle:      false,      // or a CSS class
+      only:        false,
+      delay:       0,
+      hoverclass:  null,
+      ghosting:    false,
+      scroll:      false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      format:      this.SERIALIZE_RULE,
+      onChange:    Prototype.emptyFunction,
+      onUpdate:    Prototype.emptyFunction
+    }, arguments[1] || {});
+
+    // clear any old sortable with same element
+    this.destroy(element);
+
+    // build options for the draggables
+    var options_for_draggable = {
+      revert:      true,
+      scroll:      options.scroll,
+      scrollSpeed: options.scrollSpeed,
+      scrollSensitivity: options.scrollSensitivity,
+      delay:       options.delay,
+      ghosting:    options.ghosting,
+      constraint:  options.constraint,
+      handle:      options.handle };
+
+    if(options.starteffect)
+      options_for_draggable.starteffect = options.starteffect;
+
+    if(options.reverteffect)
+      options_for_draggable.reverteffect = options.reverteffect;
+    else
+      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+        element.style.top  = 0;
+        element.style.left = 0;
+      };
+
+    if(options.endeffect)
+      options_for_draggable.endeffect = options.endeffect;
+
+    if(options.zindex)
+      options_for_draggable.zindex = options.zindex;
+
+    // build options for the droppables  
+    var options_for_droppable = {
+      overlap:     options.overlap,
+      containment: options.containment,
+      tree:        options.tree,
+      hoverclass:  options.hoverclass,
+      onHover:     Sortable.onHover
+    }
+    
+    var options_for_tree = {
+      onHover:      Sortable.onEmptyHover,
+      overlap:      options.overlap,
+      containment:  options.containment,
+      hoverclass:   options.hoverclass
+    }
+
+    // fix for gecko engine
+    Element.cleanWhitespace(element); 
+
+    options.draggables = [];
+    options.droppables = [];
+
+    // drop on empty handling
+    if(options.dropOnEmpty || options.tree) {
+      Droppables.add(element, options_for_tree);
+      options.droppables.push(element);
+    }
+
+    (this.findElements(element, options) || []).each( function(e) {
+      // handles are per-draggable
+      var handle = options.handle ? 
+        $(e).down('.'+options.handle,0) : e;    
+      options.draggables.push(
+        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+      Droppables.add(e, options_for_droppable);
+      if(options.tree) e.treeNode = element;
+      options.droppables.push(e);      
+    });
+    
+    if(options.tree) {
+      (Sortable.findTreeElements(element, options) || []).each( function(e) {
+        Droppables.add(e, options_for_tree);
+        e.treeNode = element;
+        options.droppables.push(e);
+      });
+    }
+
+    // keep reference
+    this.sortables[element.id] = options;
+
+    // for onupdate
+    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+  },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    return Element.findChildren(
+      element, options.only, options.tree ? true : false, options.tag);
+  },
+  
+  findTreeElements: function(element, options) {
+    return Element.findChildren(
+      element, options.only, options.tree ? true : false, options.treeTag);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(Element.isParent(dropon, element)) return;
+
+    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+      return;
+    } else if(overlap>0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, dropon);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    } else {
+      Sortable.mark(dropon, 'after');
+      var nextElement = dropon.nextSibling || null;
+      if(nextElement != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, nextElement);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    }
+  },
+  
+  onEmptyHover: function(element, dropon, overlap) {
+    var oldParentNode = element.parentNode;
+    var droponOptions = Sortable.options(dropon);
+        
+    if(!Element.isParent(dropon, element)) {
+      var index;
+      
+      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
+      var child = null;
+            
+      if(children) {
+        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+        
+        for (index = 0; index < children.length; index += 1) {
+          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+            offset -= Element.offsetSize (children[index], droponOptions.overlap);
+          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+            child = index + 1 < children.length ? children[index + 1] : null;
+            break;
+          } else {
+            child = children[index];
+            break;
+          }
+        }
+      }
+      
+      dropon.insertBefore(element, child);
+      
+      Sortable.options(oldParentNode).onChange(element);
+      droponOptions.onChange(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Sortable._marker.hide();
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable && !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = 
+        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
+          hide().addClassName('dropmarker').setStyle({position:'absolute'});
+      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
+    
+    if(position=='after')
+      if(sortable.overlap == 'horizontal') 
+        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
+      else
+        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
+    
+    Sortable._marker.show();
+  },
+  
+  _tree: function(element, options, parent) {
+    var children = Sortable.findElements(element, options) || [];
+  
+    for (var i = 0; i < children.length; ++i) {
+      var match = children[i].id.match(options.format);
+
+      if (!match) continue;
+      
+      var child = {
+        id: encodeURIComponent(match ? match[1] : null),
+        element: element,
+        parent: parent,
+        children: [],
+        position: parent.children.length,
+        container: $(children[i]).down(options.treeTag)
+      }
+      
+      /* Get the element containing the children and recurse over it */
+      if (child.container)
+        this._tree(child.container, options, child)
+      
+      parent.children.push (child);
+    }
+
+    return parent; 
+  },
+
+  tree: function(element) {
+    element = $(element);
+    var sortableOptions = this.options(element);
+    var options = Object.extend({
+      tag: sortableOptions.tag,
+      treeTag: sortableOptions.treeTag,
+      only: sortableOptions.only,
+      name: element.id,
+      format: sortableOptions.format
+    }, arguments[1] || {});
+    
+    var root = {
+      id: null,
+      parent: null,
+      children: [],
+      container: element,
+      position: 0
+    }
+    
+    return Sortable._tree(element, options, root);
+  },
+
+  /* Construct a [i] index for a particular node */
+  _constructIndex: function(node) {
+    var index = '';
+    do {
+      if (node.id) index = '[' + node.position + ']' + index;
+    } while ((node = node.parent) != null);
+    return index;
+  },
+
+  sequence: function(element) {
+    element = $(element);
+    var options = Object.extend(this.options(element), arguments[1] || {});
+    
+    return $(this.findElements(element, options) || []).map( function(item) {
+      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+    });
+  },
+
+  setSequence: function(element, new_sequence) {
+    element = $(element);
+    var options = Object.extend(this.options(element), arguments[2] || {});
+    
+    var nodeMap = {};
+    this.findElements(element, options).each( function(n) {
+        if (n.id.match(options.format))
+            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+        n.parentNode.removeChild(n);
+    });
+   
+    new_sequence.each(function(ident) {
+      var n = nodeMap[ident];
+      if (n) {
+        n[1].appendChild(n[0]);
+        delete nodeMap[ident];
+      }
+    });
+  },
+  
+  serialize: function(element) {
+    element = $(element);
+    var options = Object.extend(Sortable.options(element), arguments[1] || {});
+    var name = encodeURIComponent(
+      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
+    
+    if (options.tree) {
+      return Sortable.tree(element, arguments[1]).children.map( function (item) {
+        return [name + Sortable._constructIndex(item) + "[id]=" + 
+                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+      }).flatten().join('&');
+    } else {
+      return Sortable.sequence(element, arguments[1]).map( function(item) {
+        return name + "[]=" + encodeURIComponent(item);
+      }).join('&');
+    }
+  }
+}
+
+// Returns true if child is contained within element
+Element.isParent = function(child, element) {
+  if (!child.parentNode || child == element) return false;
+  if (child.parentNode == element) return true;
+  return Element.isParent(child.parentNode, element);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {    
+  if(!element.hasChildNodes()) return null;
+  tagName = tagName.toUpperCase();
+  if(only) only = [only].flatten();
+  var elements = [];
+  $A(element.childNodes).each( function(e) {
+    if(e.tagName && e.tagName.toUpperCase()==tagName &&
+      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+        elements.push(e);
+    if(recursive) {
+      var grandchildren = Element.findChildren(e, only, recursive, tagName);
+      if(grandchildren) elements.push(grandchildren);
+    }
+  });
+
+  return (elements.length>0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
+}
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/effects.js
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/effects.js	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/effects.js	(revision 540)
@@ -0,0 +1,960 @@
+// script.aculo.us effects.js v1.6.5, Wed Nov 08 14:17:49 CET 2006
+
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+//  Justin Palmer (http://encytemedia.com/)
+//  Mark Pilgrim (http://diveintomark.org/)
+//  Martin Bialasinki
+// 
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/ 
+
+// converts rgb() and #xxx to #xxxxxx format,  
+// returns self (or first argument) if not convertable  
+String.prototype.parseColor = function() {  
+  var color = '#';  
+  if(this.slice(0,4) == 'rgb(') {  
+    var cols = this.slice(4,this.length-1).split(',');  
+    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
+  } else {  
+    if(this.slice(0,1) == '#') {  
+      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
+      if(this.length==7) color = this.toLowerCase();  
+    }  
+  }  
+  return(color.length==7 ? color : (arguments[0] || this));  
+}
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+  }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 
+        Element.collectTextNodesIgnoreClass(node, className) : ''));
+  }).flatten().join('');
+}
+
+Element.setContentZoom = function(element, percent) {
+  element = $(element);  
+  element.setStyle({fontSize: (percent/100) + 'em'});   
+  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+  return element;
+}
+
+Element.getOpacity = function(element){
+  element = $(element);
+  var opacity;
+  if (opacity = element.getStyle('opacity'))  
+    return parseFloat(opacity);  
+  if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))  
+    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
+  return 1.0;  
+}
+
+Element.setOpacity = function(element, value){  
+  element= $(element);  
+  if (value == 1){
+    element.setStyle({ opacity: 
+      (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
+      0.999999 : 1.0 });
+    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
+      element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
+  } else {  
+    if(value < 0.00001) value = 0;  
+    element.setStyle({opacity: value});
+    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
+      element.setStyle(
+        { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
+            'alpha(opacity='+value*100+')' });  
+  }
+  return element;
+}  
+ 
+Element.getInlineOpacity = function(element){  
+  return $(element).style.opacity || '';
+}  
+
+Element.forceRerendering = function(element) {
+  try {
+    element = $(element);
+    var n = document.createTextNode(' ');
+    element.appendChild(n);
+    element.removeChild(n);
+  } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+// Don't patch Array, this breaks jQuery library
+// Maybe it will cause some problems with drag and drop functionality
+// provided by scriptaculous library, but right now it's working ;-)
+
+// Array.prototype.call = function() {
+//   var args = arguments;
+//   this.each(function(f){ f.apply(this, args) });
+// }
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+  _elementDoesNotExistError: {
+    name: 'ElementDoesNotExistError',
+    message: 'The specified DOM element does not exist, but is required for this effect to operate'
+  },
+  tagifyText: function(element) {
+    if(typeof Builder == 'undefined')
+      throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
+      
+    var tagifyStyle = 'position:relative';
+    if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
+    
+    element = $(element);
+    $A(element.childNodes).each( function(child) {
+      if(child.nodeType==3) {
+        child.nodeValue.toArray().each( function(character) {
+          element.insertBefore(
+            Builder.node('span',{style: tagifyStyle},
+              character == ' ' ? String.fromCharCode(160) : character), 
+              child);
+        });
+        Element.remove(child);
+      }
+    });
+  },
+  multiple: function(element, effect) {
+    var elements;
+    if(((typeof element == 'object') || 
+        (typeof element == 'function')) && 
+       (element.length))
+      elements = element;
+    else
+      elements = $(element).childNodes;
+      
+    var options = Object.extend({
+      speed: 0.1,
+      delay: 0.0
+    }, arguments[2] || {});
+    var masterDelay = options.delay;
+
+    $A(elements).each( function(element, index) {
+      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+    });
+  },
+  PAIRS: {
+    'slide':  ['SlideDown','SlideUp'],
+    'blind':  ['BlindDown','BlindUp'],
+    'appear': ['Appear','Fade']
+  },
+  toggle: function(element, effect) {
+    element = $(element);
+    effect = (effect || 'appear').toLowerCase();
+    var options = Object.extend({
+      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+    }, arguments[2] || {});
+    Effect[element.visible() ? 
+      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+  }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {
+  linear: Prototype.K,
+  sinoidal: function(pos) {
+    return (-Math.cos(pos*Math.PI)/2) + 0.5;
+  },
+  reverse: function(pos) {
+    return 1-pos;
+  },
+  flicker: function(pos) {
+    return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+  },
+  wobble: function(pos) {
+    return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+  },
+  pulse: function(pos, pulses) { 
+    pulses = pulses || 5; 
+    return (
+      Math.round((pos % (1/pulses)) * pulses) == 0 ? 
+            ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : 
+        1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
+      );
+  },
+  none: function(pos) {
+    return 0;
+  },
+  full: function(pos) {
+    return 1;
+  }
+};
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+  initialize: function() {
+    this.effects  = [];
+    this.interval = null;
+  },
+  _each: function(iterator) {
+    this.effects._each(iterator);
+  },
+  add: function(effect) {
+    var timestamp = new Date().getTime();
+    
+    var position = (typeof effect.options.queue == 'string') ? 
+      effect.options.queue : effect.options.queue.position;
+    
+    switch(position) {
+      case 'front':
+        // move unstarted effects after this effect  
+        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+            e.startOn  += effect.finishOn;
+            e.finishOn += effect.finishOn;
+          });
+        break;
+      case 'with-last':
+        timestamp = this.effects.pluck('startOn').max() || timestamp;
+        break;
+      case 'end':
+        // start effect after last queued effect has finished
+        timestamp = this.effects.pluck('finishOn').max() || timestamp;
+        break;
+    }
+    
+    effect.startOn  += timestamp;
+    effect.finishOn += timestamp;
+
+    if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+      this.effects.push(effect);
+    
+    if(!this.interval) 
+      this.interval = setInterval(this.loop.bind(this), 40);
+  },
+  remove: function(effect) {
+    this.effects = this.effects.reject(function(e) { return e==effect });
+    if(this.effects.length == 0) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+  },
+  loop: function() {
+    var timePos = new Date().getTime();
+    this.effects.invoke('loop', timePos);
+  }
+});
+
+Effect.Queues = {
+  instances: $H(),
+  get: function(queueName) {
+    if(typeof queueName != 'string') return queueName;
+    
+    if(!this.instances[queueName])
+      this.instances[queueName] = new Effect.ScopedQueue();
+      
+    return this.instances[queueName];
+  }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+  transition: Effect.Transitions.sinoidal,
+  duration:   1.0,   // seconds
+  fps:        25.0,  // max. 25fps due to Effect.Queue implementation
+  sync:       false, // true for combining
+  from:       0.0,
+  to:         1.0,
+  delay:      0.0,
+  queue:      'parallel'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+  position: null,
+  start: function(options) {
+    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+    this.currentFrame = 0;
+    this.state        = 'idle';
+    this.startOn      = this.options.delay*1000;
+    this.finishOn     = this.startOn + (this.options.duration*1000);
+    this.event('beforeStart');
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).add(this);
+  },
+  loop: function(timePos) {
+    if(timePos >= this.startOn) {
+      if(timePos >= this.finishOn) {
+        this.render(1.0);
+        this.cancel();
+        this.event('beforeFinish');
+        if(this.finish) this.finish(); 
+        this.event('afterFinish');
+        return;  
+      }
+      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
+      var frame = Math.round(pos * this.options.fps * this.options.duration);
+      if(frame > this.currentFrame) {
+        this.render(pos);
+        this.currentFrame = frame;
+      }
+    }
+  },
+  render: function(pos) {
+    if(this.state == 'idle') {
+      this.state = 'running';
+      this.event('beforeSetup');
+      if(this.setup) this.setup();
+      this.event('afterSetup');
+    }
+    if(this.state == 'running') {
+      if(this.options.transition) pos = this.options.transition(pos);
+      pos *= (this.options.to-this.options.from);
+      pos += this.options.from;
+      this.position = pos;
+      this.event('beforeUpdate');
+      if(this.update) this.update(pos);
+      this.event('afterUpdate');
+    }
+  },
+  cancel: function() {
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).remove(this);
+    this.state = 'finished';
+  },
+  event: function(eventName) {
+    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+    if(this.options[eventName]) this.options[eventName](this);
+  },
+  inspect: function() {
+    return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
+  }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+  initialize: function(effects) {
+    this.effects = effects || [];
+    this.start(arguments[1]);
+  },
+  update: function(position) {
+    this.effects.invoke('render', position);
+  },
+  finish: function(position) {
+    this.effects.each( function(effect) {
+      effect.render(1.0);
+      effect.cancel();
+      effect.event('beforeFinish');
+      if(effect.finish) effect.finish(position);
+      effect.event('afterFinish');
+    });
+  }
+});
+
+Effect.Event = Class.create();
+Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
+  initialize: function() {
+    var options = Object.extend({
+      duration: 0
+    }, arguments[0] || {});
+    this.start(options);
+  },
+  update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    // make this work on IE on elements without 'layout'
+    if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
+      this.element.setStyle({zoom: 1});
+    var options = Object.extend({
+      from: this.element.getOpacity() || 0.0,
+      to:   1.0
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  update: function(position) {
+    this.element.setOpacity(position);
+  }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({
+      x:    0,
+      y:    0,
+      mode: 'relative'
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Bug in Opera: Opera returns the "real" position of a static element or
+    // relative element that does not have top/left explicitly set.
+    // ==> Always set top and left for position relative elements in your stylesheets 
+    // (to 0 if you do not need them) 
+    this.element.makePositioned();
+    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
+    if(this.options.mode == 'absolute') {
+      // absolute movement, so we need to calc deltaX and deltaY
+      this.options.x = this.options.x - this.originalLeft;
+      this.options.y = this.options.y - this.originalTop;
+    }
+  },
+  update: function(position) {
+    this.element.setStyle({
+      left: Math.round(this.options.x  * position + this.originalLeft) + 'px',
+      top:  Math.round(this.options.y  * position + this.originalTop)  + 'px'
+    });
+  }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+  return new Effect.Move(element, 
+    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+  initialize: function(element, percent) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({
+      scaleX: true,
+      scaleY: true,
+      scaleContent: true,
+      scaleFromCenter: false,
+      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
+      scaleFrom: 100.0,
+      scaleTo:   percent
+    }, arguments[2] || {});
+    this.start(options);
+  },
+  setup: function() {
+    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+    this.elementPositioning = this.element.getStyle('position');
+    
+    this.originalStyle = {};
+    ['top','left','width','height','fontSize'].each( function(k) {
+      this.originalStyle[k] = this.element.style[k];
+    }.bind(this));
+      
+    this.originalTop  = this.element.offsetTop;
+    this.originalLeft = this.element.offsetLeft;
+    
+    var fontSize = this.element.getStyle('font-size') || '100%';
+    ['em','px','%','pt'].each( function(fontSizeType) {
+      if(fontSize.indexOf(fontSizeType)>0) {
+        this.fontSize     = parseFloat(fontSize);
+        this.fontSizeType = fontSizeType;
+      }
+    }.bind(this));
+    
+    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+    
+    this.dims = null;
+    if(this.options.scaleMode=='box')
+      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+    if(/^content/.test(this.options.scaleMode))
+      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+    if(!this.dims)
+      this.dims = [this.options.scaleMode.originalHeight,
+                   this.options.scaleMode.originalWidth];
+  },
+  update: function(position) {
+    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+    if(this.options.scaleContent && this.fontSize)
+      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+  },
+  finish: function(position) {
+    if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+  },
+  setDimensions: function(height, width) {
+    var d = {};
+    if(this.options.scaleX) d.width = Math.round(width) + 'px';
+    if(this.options.scaleY) d.height = Math.round(height) + 'px';
+    if(this.options.scaleFromCenter) {
+      var topd  = (height - this.dims[0])/2;
+      var leftd = (width  - this.dims[1])/2;
+      if(this.elementPositioning == 'absolute') {
+        if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+        if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+      } else {
+        if(this.options.scaleY) d.top = -topd + 'px';
+        if(this.options.scaleX) d.left = -leftd + 'px';
+      }
+    }
+    this.element.setStyle(d);
+  }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Prevent executing on elements not in the layout flow
+    if(this.element.getStyle('display')=='none') { this.cancel(); return; }
+    // Disable background image during the effect
+    this.oldStyle = {
+      backgroundImage: this.element.getStyle('background-image') };
+    this.element.setStyle({backgroundImage: 'none'});
+    if(!this.options.endcolor)
+      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+    if(!this.options.restorecolor)
+      this.options.restorecolor = this.element.getStyle('background-color');
+    // init color calculations
+    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+  },
+  update: function(position) {
+    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+      return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+  },
+  finish: function() {
+    this.element.setStyle(Object.extend(this.oldStyle, {
+      backgroundColor: this.options.restorecolor
+    }));
+  }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    this.start(arguments[1] || {});
+  },
+  setup: function() {
+    Position.prepare();
+    var offsets = Position.cumulativeOffset(this.element);
+    if(this.options.offset) offsets[1] += this.options.offset;
+    var max = window.innerHeight ? 
+      window.height - window.innerHeight :
+      document.body.scrollHeight - 
+        (document.documentElement.clientHeight ? 
+          document.documentElement.clientHeight : document.body.clientHeight);
+    this.scrollStart = Position.deltaY;
+    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+  },
+  update: function(position) {
+    Position.prepare();
+    window.scrollTo(Position.deltaX, 
+      this.scrollStart + (position*this.delta));
+  }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+  element = $(element);
+  var oldOpacity = element.getInlineOpacity();
+  var options = Object.extend({
+  from: element.getOpacity() || 1.0,
+  to:   0.0,
+  afterFinishInternal: function(effect) { 
+    if(effect.options.to!=0) return;
+    effect.element.hide().setStyle({opacity: oldOpacity}); 
+  }}, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+  element = $(element);
+  var options = Object.extend({
+  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+  to:   1.0,
+  // force Safari to render floated elements properly
+  afterFinishInternal: function(effect) {
+    effect.element.forceRerendering();
+  },
+  beforeSetup: function(effect) {
+    effect.element.setOpacity(effect.options.from).show(); 
+  }}, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+  element = $(element);
+  var oldStyle = { 
+    opacity: element.getInlineOpacity(), 
+    position: element.getStyle('position'),
+    top:  element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height
+  };
+  return new Effect.Parallel(
+   [ new Effect.Scale(element, 200, 
+      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
+     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
+     Object.extend({ duration: 1.0, 
+      beforeSetupInternal: function(effect) {
+        Position.absolutize(effect.effects[0].element)
+      },
+      afterFinishInternal: function(effect) {
+         effect.effects[0].element.hide().setStyle(oldStyle); }
+     }, arguments[1] || {})
+   );
+}
+
+Effect.BlindUp = function(element) {
+  element = $(element);
+  element.makeClipping();
+  return new Effect.Scale(element, 0,
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      restoreAfterFinish: true,
+      afterFinishInternal: function(effect) {
+        effect.element.hide().undoClipping();
+      } 
+    }, arguments[1] || {})
+  );
+}
+
+Effect.BlindDown = function(element) {
+  element = $(element);
+  var elementDimensions = element.getDimensions();
+  return new Effect.Scale(element, 100, Object.extend({ 
+    scaleContent: false, 
+    scaleX: false,
+    scaleFrom: 0,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+    restoreAfterFinish: true,
+    afterSetup: function(effect) {
+      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
+    },  
+    afterFinishInternal: function(effect) {
+      effect.element.undoClipping();
+    }
+  }, arguments[1] || {}));
+}
+
+Effect.SwitchOff = function(element) {
+  element = $(element);
+  var oldOpacity = element.getInlineOpacity();
+  return new Effect.Appear(element, Object.extend({
+    duration: 0.4,
+    from: 0,
+    transition: Effect.Transitions.flicker,
+    afterFinishInternal: function(effect) {
+      new Effect.Scale(effect.element, 1, { 
+        duration: 0.3, scaleFromCenter: true,
+        scaleX: false, scaleContent: false, restoreAfterFinish: true,
+        beforeSetup: function(effect) { 
+          effect.element.makePositioned().makeClipping();
+        },
+        afterFinishInternal: function(effect) {
+          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+        }
+      })
+    }
+  }, arguments[1] || {}));
+}
+
+Effect.DropOut = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.getStyle('top'),
+    left: element.getStyle('left'),
+    opacity: element.getInlineOpacity() };
+  return new Effect.Parallel(
+    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
+      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+    Object.extend(
+      { duration: 0.5,
+        beforeSetup: function(effect) {
+          effect.effects[0].element.makePositioned(); 
+        },
+        afterFinishInternal: function(effect) {
+          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+        } 
+      }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.getStyle('top'),
+    left: element.getStyle('left') };
+    return new Effect.Move(element, 
+      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+        effect.element.undoPositioned().setStyle(oldStyle);
+  }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+  element = $(element).cleanWhitespace();
+  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+  var oldInnerBottom = element.down().getStyle('bottom');
+  var elementDimensions = element.getDimensions();
+  return new Effect.Scale(element, 100, Object.extend({ 
+    scaleContent: false, 
+    scaleX: false, 
+    scaleFrom: window.opera ? 0 : 1,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+    restoreAfterFinish: true,
+    afterSetup: function(effect) {
+      effect.element.makePositioned();
+      effect.element.down().makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
+    },
+    afterUpdateInternal: function(effect) {
+      effect.element.down().setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
+    },
+    afterFinishInternal: function(effect) {
+      effect.element.undoClipping().undoPositioned();
+      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+    }, arguments[1] || {})
+  );
+}
+
+Effect.SlideUp = function(element) {
+  element = $(element).cleanWhitespace();
+  var oldInnerBottom = element.down().getStyle('bottom');
+  return new Effect.Scale(element, window.opera ? 0 : 1,
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleMode: 'box',
+    scaleFrom: 100,
+    restoreAfterFinish: true,
+    beforeStartInternal: function(effect) {
+      effect.element.makePositioned();
+      effect.element.down().makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping().show();
+    },  
+    afterUpdateInternal: function(effect) {
+      effect.element.down().setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' });
+    },
+    afterFinishInternal: function(effect) {
+      effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
+      effect.element.down().undoPositioned();
+    }
+   }, arguments[1] || {})
+  );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish 
+Effect.Squish = function(element) {
+  return new Effect.Scale(element, window.opera ? 1 : 0, { 
+    restoreAfterFinish: true,
+    beforeSetup: function(effect) {
+      effect.element.makeClipping(); 
+    },  
+    afterFinishInternal: function(effect) {
+      effect.element.hide().undoClipping(); 
+    }
+  });
+}
+
+Effect.Grow = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.full
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();    
+  var initialMoveX, initialMoveY;
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      break;
+    case 'top-right':
+      initialMoveX = dims.width;
+      initialMoveY = moveY = 0;
+      moveX = -dims.width;
+      break;
+    case 'bottom-left':
+      initialMoveX = moveX = 0;
+      initialMoveY = dims.height;
+      moveY = -dims.height;
+      break;
+    case 'bottom-right':
+      initialMoveX = dims.width;
+      initialMoveY = dims.height;
+      moveX = -dims.width;
+      moveY = -dims.height;
+      break;
+    case 'center':
+      initialMoveX = dims.width / 2;
+      initialMoveY = dims.height / 2;
+      moveX = -dims.width / 2;
+      moveY = -dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Move(element, {
+    x: initialMoveX,
+    y: initialMoveY,
+    duration: 0.01, 
+    beforeSetup: function(effect) {
+      effect.element.hide().makeClipping().makePositioned();
+    },
+    afterFinishInternal: function(effect) {
+      new Effect.Parallel(
+        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+          new Effect.Scale(effect.element, 100, {
+            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
+            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+        ], Object.extend({
+             beforeSetup: function(effect) {
+               effect.effects[0].element.setStyle({height: '0px'}).show(); 
+             },
+             afterFinishInternal: function(effect) {
+               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 
+             }
+           }, options)
+      )
+    }
+  });
+}
+
+Effect.Shrink = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.none
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      moveX = moveY = 0;
+      break;
+    case 'top-right':
+      moveX = dims.width;
+      moveY = 0;
+      break;
+    case 'bottom-left':
+      moveX = 0;
+      moveY = dims.height;
+      break;
+    case 'bottom-right':
+      moveX = dims.width;
+      moveY = dims.height;
+      break;
+    case 'center':  
+      moveX = dims.width / 2;
+      moveY = dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Parallel(
+    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+    ], Object.extend({            
+         beforeStartInternal: function(effect) {
+           effect.effects[0].element.makePositioned().makeClipping(); 
+         },
+         afterFinishInternal: function(effect) {
+           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+       }, options)
+  );
+}
+
+Effect.Pulsate = function(element) {
+  element = $(element);
+  var options    = arguments[1] || {};
+  var oldOpacity = element.getInlineOpacity();
+  var transition = options.transition || Effect.Transitions.sinoidal;
+  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
+  reverser.bind(transition);
+  return new Effect.Opacity(element, 
+    Object.extend(Object.extend({  duration: 2.0, from: 0,
+      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+    }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height };
+  element.makeClipping();
+  return new Effect.Scale(element, 5, Object.extend({   
+    scaleContent: false,
+    scaleX: false,
+    afterFinishInternal: function(effect) {
+    new Effect.Scale(element, 1, { 
+      scaleContent: false, 
+      scaleY: false,
+      afterFinishInternal: function(effect) {
+        effect.element.hide().undoClipping().setStyle(oldStyle);
+      } });
+  }}, arguments[1] || {}));
+};
+
+['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass'].each( 
+  function(f) { Element.Methods[f] = Element[f]; }
+);
+
+Element.Methods.visualEffect = function(element, effect, options) {
+  s = effect.gsub(/_/, '-').camelize();
+  effect_class = s.charAt(0).toUpperCase() + s.substring(1);
+  new Effect[effect_class](element, options);
+  return $(element);
+};
+
+Element.addMethods();
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/plonetabs.css
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/plonetabs.css	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/plonetabs.css	(revision 540)
@@ -0,0 +1,312 @@
+/* <dtml-with base_properties> (do not remove this :) */
+/* <dtml-call "REQUEST.set('portal_url', portal_url())"> (not this either :) */
+
+/*
+***************************************************************************************************
+*                                                                                                 *
+*   This doc is outdated now, don't read it!!!                                                    *
+*                                                                                                 *
+*   There are two global self-excluding classes: 'viewing' and 'sorting',                         *
+*   one local class 'editing' for menu items which are expanded for editing,                      *
+*   and one more local class 'adding' for adding form when it's in open state.                    *
+*                                                                                                 *
+*   Reorder action are always available and it TEMPORARY stop other actions on the form:          *
+*       editing items                                                                             *
+*       deleting items                                                                            *
+*       toggling item visibility                                                                  *
+*       adding item                                                                               *
+*                                                                                                 *
+*   After returning to 'viewing' class from 'sorting' all items become to the same state they     *
+*   were before sorting (in editing state or not).                                                *
+*                                                                                                 *
+*   And the same behaviour for adding form: form state ('adding' or not) become the same it was   *
+*   before sorting.                                                                               *
+*                                                                                                 *
+*   All actions except reordering could be done at any time in relationship to each other.        *
+*                                                                                                 *
+***************************************************************************************************
+*/
+
+/*---------- QUINTAGROUP PLONE TABS LAYOUT ----------*/
+.qPloneTabs ul {
+  list-style-type:none;
+  list-style-image:none;
+  margin-left:18px;
+  }
+.kssTabsActive ul {
+  margin-left:0;
+  }
+.titleWrapper,
+.kssTabsActive .editing .titleWrapper,
+.qPloneTabs .delete,
+.kssTabsActive .edit-fields-wrapper,
+.kssTabsActive .bridge,
+.qPloneTabs .drag-handle,
+.kssTabsActive .field-action,
+.kssTabsActive .collapsedBlock .contentAdvanced,
+#addaction .headerAdvanced,
+.qPloneTabs .headerAdvanced,
+.kssTabsActive .add-controls,
+.kssTabsActive .kssHidden,
+.kssTabsActive #autogeneration_controls {
+  display:none;
+  }
+.kssTabsActive .titleWrapper,
+.kssTabsActive li.editing .edit-fields-wrapper,
+.kssTabsActive .adding .bridge,
+.adding .field-action,
+.adding .collapseAdvanced,
+.kssTabsActive .headerAdvanced,
+.kssTabsActive #addaction.adding .headerAdvanced,
+.adding .add-controls,
+.kssTabsActive .expandedBlock .contentAdvanced,
+.kssTabsActive .field-action label {
+  display:block;
+  }
+.kssTabsActive li:hover .bridge,
+.kssTabsActive li:hover .delete,
+.kssTabsActive li:hover .drag-handle,
+.kssTabsActive #app #addaction.adding      .error-container {
+  display:inline;
+  }
+
+/*----- Items List Section -----*/
+#tabslist li {
+  clear:left;
+  margin:0.1em 6.5em 0.1em 0;
+  position:relative;
+  width:35em;
+  }
+.kssTabsActive #tabslist li {
+  margin-left:-12px;
+  }
+#tabslist li span {
+  display:block;
+  }
+.qPloneTabs .delete {
+  background:url(++resource++trash.gif) 5px 50% no-repeat;
+  border:0;
+  font-size:80%;
+  left:100%;
+  line-height:1.8em;
+  padding:0 0 0 18px;
+  position:absolute;
+  }
+.qPloneTabs .editing form {
+  margin:0 0 0 30px;
+  }
+.qPloneTabs dl {
+  margin:0;
+  padding:0;
+  }
+.qPloneTabs dl label {
+  display:none;
+  }
+.qPloneTabs dl dl label {
+  display:block;
+  } 
+.qPloneTabs dd {
+  margin:0;
+  }
+.qPloneTabs dd dl {
+  margin:0.5em 0;
+  }
+.qPloneTabs input[type="text"] {
+  padding:2px 0;
+  text-indent:3px;
+  //text-indent:0;
+  width:99.5%;
+  }
+.qPloneTabs input:focus {
+  border:1px inset threedface !important;
+  }
+.qPloneTabs form div {
+  line-height:1.4em;
+  }
+.qPloneTabs div input {
+  margin:0 0.1em 0 0;
+  width:auto;
+  }
+.edit-controls,
+.add-controls {
+  margin:0.25em 0;
+  }
+
+/*--- Title Wrapper ---*/
+.titleWrapper {
+  background-color:#fff;
+  margin-left:30px;
+  overflow:hidden;
+  padding:1px 5px;
+  white-space:nowrap;
+  }
+.titleWrapper:hover {
+  background-color:#FFFFDD;
+  border:1px inset threedface;
+  cursor:text;
+  padding:0 4px;
+  }
+.tab-title {
+  float:left;
+  min-width:35%;
+  padding-right:0.5em;
+  position:relative;
+  z-index:1;
+  }
+#app .invisible span.tab-title {
+  color:#aaa;
+  }
+.url-helper {
+  color:#aaa;
+  font-size:80%;
+  overflow:hidden;
+  padding-right:0.5em;
+  }
+
+/*--- Bridge ---*/
+.qPloneTabs .bridge {
+  left:-1.5em;
+  //left:-1.7em;
+  position:absolute;
+  top:0.2em;
+  width:3em;
+  }
+.kssTabsActive .qPloneTabs .bridge {
+  left:0;
+  width:30px;
+  }
+/* IE need this to remove the padding around the check box */
+.kssTabsActive .qPloneTabs .bridge input {
+  height:13px;
+  width:13px;
+  }
+.drag-handle {
+  cursor:move;
+  float:left;
+  margin-right:0.2em;
+  //margin-top:0.2em;
+  }
+
+/*----- Edit Item Section -----*/
+#app dl.collapseAdvanced {
+  line-height:1em;
+  position:relative;
+  }
+#app dl.collapseAdvanced dt.headerAdvanced {
+  background:url(++resource++treeCollapsed.gif) no-repeat 6px 50%;
+  color:#436976;
+  cursor:pointer;
+  font-size: 90%;
+  font-weight:normal;
+  line-height:2em;
+  margin:0;
+  padding: 0 6px 0 22px;
+  position:absolute;
+  right:0;
+  top:0;
+  }
+#app dl.expandedBlock dt.headerAdvanced {
+  background-image:url(++resource++treeExpanded.gif);
+  }
+#app dl.collapseAdvanced dd.contentAdvanced {
+  background-color:#dee7ec;
+  padding:1px 1.5em 0.5em;
+  zoom:1;
+  }
+#app dl.expandedBlock dd.contentAdvanced {
+  display: block;
+  }
+
+/*----- Add Action Section -----*/
+#addaction {
+  margin-left:18px;
+  position:relative;
+  width:35em;
+  }
+.kssTabsActive #addaction {
+  width:32.5em;
+  }
+#addaction .field-action {
+  position:relative;
+  }
+#addaction .field-action label {
+  bottom:0;
+  color: #8cacbb;
+  font-size:80%;
+  font-weight:normal;
+  line-height:2.1em;
+  position:absolute;
+  right:0.5em;
+  }
+#addaction.adding .field-action input {
+  margin-top:0.1em; 
+  padding-right:9em;
+  width:23.3em;
+  }
+#addaction .bridge {
+  left:-18px;
+  top:0;
+  }
+  
+/*----- Autogenerated Section -----*/
+#autogeneration_section {
+  margin-top:1em;
+  }
+#roottabs {
+  margin-left:0;
+  }
+#roottabs li {
+  margin:0.1em 6.5em 0.1em 0;
+  padding-left:18px;
+  position:relative;
+  width:35em;
+  }
+#roottabs li:hover {
+  background-color:#FFD;
+  cursor:default;
+  }
+#roottabs .bridge {
+  left:0;  
+  }
+#roottabs li:hover .bridge {
+  display:block;
+  }
+#autogeneration_controls {
+  padding-left:18px;
+  }
+.qPloneTabs .visibility {
+  height:13px;
+  width:13px;
+  }
+
+/*----- Error Messeges -----*/
+.qPloneTabs dl.error {
+  border:1px solid #D80;
+  font-size:0.91em;
+  margin:2px 0 0;
+  }
+.collapseAdvanced dl.error {
+  margin-top:2em !important;  
+  }
+.qPloneTabs dl.error dt {
+  color:#fff;
+  float:left;
+  line-height:1.5em;
+  margin-right:0.5em;
+  padding:0 0.5em;
+  }
+.qPloneTabs dl.error dd {
+  padding:0 0.5em;
+  }
+.qPloneTabs dl.error input[type="text"] {
+  margin:0 -0.5em;
+  width:102.4%;
+  }
+.qPloneTabs .contentAdvanced dl.error input[type="text"] {
+  width:102.5%;
+  }
+#addaction.adding .error #actaction {
+  width:26.4em;
+  }
+
+/* </dtml-with> (do not remove this either :) */
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/plonetabs.kss
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/plonetabs.kss	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/resources/plonetabs.kss	(revision 540)
@@ -0,0 +1,180 @@
+/* <dtml-call expr="RESPONSE.setHeader('pragma','no-cache')"> */
+
+/*
+    The above trick to let this file not be cached does not work, unless this file is renamed to have
+    a kss.dtml extension.
+*/
+
+
+/* General sheet for Plone Tabs AJAX configlet */
+
+/* Add 'kssTabsActive' class to application root element
+   to enable ajax styles when kss is available */
+#plonetabs_form:load {
+  action-client: addClass;
+  addClass-value: kssTabsActive;
+}
+
+
+/* Add allowMultiSubmit class to submit buttons
+   to avoid those annoying warnings about form 'multisubmittion' */
+.add-controls input:load {
+    action-client: addClass;
+    addClass-value: allowMultiSubmit;
+}
+
+.edit-controls input:load {
+    action-client: addClass;
+    addClass-value: allowMultiSubmit;
+}
+
+/* Collapse expanded sections on load */
+.collapseAdvanced:load {
+    action-client: removeClass;
+    removeClass-value: expandedBlock;
+    action-client: addClass;
+    addClass-value: collapsedBlock;
+}
+
+/* Remember portal actions category */
+#select_category:load {
+    action-client: setStateVar;
+    setStateVar-varname: plonetabs-category;
+    setStateVar-value: currentFormVar();
+}
+
+/* Change portal actions category page */
+#select_category:change {
+    action-server: plonetabs-changeCategory;
+    plonetabs-changeCategory-cat_name: currentFormVar();
+    plonetabs-changeCategory-error: plonetabs-handleServerError;
+}
+
+ /*Hovering for IE especially */
+/*#app li:mouseover {
+    action-client: addClass;
+    addClass-value: onHover;
+}
+
+#app li:mouseout {
+    action-client: removeClass;
+    removeClass-value: onHover;
+}*/
+
+
+/* Automatically generated tabs */
+#generated_tabs:click {
+    action-server: plonetabs-toggleGeneratedTabs;
+    plonetabs-toggleGeneratedTabs-field: disable_folder_sections;
+    plonetabs-toggleGeneratedTabs-checked: currentFormVar();
+    plonetabs-toggleGeneratedTabs-error: plonetabs-handleServerError;
+}
+
+#nonfolderish_tabs:click {
+    action-server: plonetabs-toggleGeneratedTabs;
+    plonetabs-toggleGeneratedTabs-field: disable_nonfolderish_sections;
+    plonetabs-toggleGeneratedTabs-checked: currentFormVar();
+    plonetabs-toggleGeneratedTabs-error: plonetabs-handleServerError;
+}
+
+/* Visibility switching */
+#tabslist input.visibility:click {
+    action-server: plonetabs-toggleActionsVisibility;
+    plonetabs-toggleActionsVisibility-id: nodeAttr(id, true);
+    plonetabs-toggleActionsVisibility-checked: currentFormVar();
+    plonetabs-toggleActionsVisibility-cat_name: stateVar(plonetabs-category);
+    plonetabs-toggleActionsVisibility-error: plonetabs-handleServerError;
+}
+
+#roottabs .visibility:click {
+    action-server: plonetabs-toggleRootsVisibility;
+    plonetabs-toggleRootsVisibility-id: nodeAttr(id, true);
+    plonetabs-toggleRootsVisibility-checked: currentFormVar();
+    plonetabs-toggleRootsVisibility-error: plonetabs-handleServerError;
+}
+
+/* Delete action link */
+#tabslist .delete:click {
+    evt-click-preventdefault: true;
+    action-server: plonetabs-deleteAction;
+    plonetabs-deleteAction-id: nodeAttr(id, true);
+    plonetabs-deleteAction-cat_name: stateVar(plonetabs-category);
+    plonetabs-deleteAction-error: plonetabs-handleServerError;
+}
+
+/* Add form rules */
+
+/* Save initial value of title field on adding form */
+#actname:load {
+    action-client: setStateVar;
+    setStateVar-varname: plonetabs-addingTitle;
+    setStateVar-value: currentFormVar();
+}
+
+#actname:focus {
+    action-client: addClass;
+    addClass-kssSelector: htmlid(addaction);
+    addClass-value: adding;
+}
+
+#buttoncancel:click {
+    evt-click-preventdefault: true;
+    action-server: plonetabs-hideAddForm;
+    plonetabs-hideAddForm-error: plonetabs-handleServerError;
+}
+
+#buttonadd:click,
+#addaction:submit {
+    evt-click-preventdefault: true;
+    action-server: plonetabs-addAction;
+    plonetabs-addAction-kssSubmitForm: currentForm();
+    plonetabs-addAction-error: plonetabs-handleServerError;
+}
+
+/* autogenerate value for id field */
+#actname:keyup {
+    action-client: plonetabs-generateId;
+    plonetabs-generateId-target: actid;
+    plonetabs-generateId-var_name: plonetabs-addingTitle;
+}
+
+/* Actions edit */
+#tabslist .titleWrapper:click {
+    evt-click-allowbubbling: true;
+    action-client: addClass;
+    addClass-kssSelector: parentnode('#tabslist li');
+    addClass-value: editing;
+}
+
+/* Edit cancel button */
+#tabslist .editcancel:click {
+    evt-click-preventdefault: true;
+    action-server: plonetabs-hideEditForm;
+    plonetabs-hideEditForm-id: nodeAttr(id, true);
+    plonetabs-hideEditForm-cat_name: stateVar(plonetabs-category);
+    plonetabs-hideEditForm-error: plonetabs-handleServerError;
+}
+
+/* Edit save button */
+#tabslist .editsave:click {
+    evt-click-preventdefault: true;
+    action-server: plonetabs-editAction;
+    plonetabs-editAction-kssSubmitForm: currentForm();
+    plonetabs-editAction-error: plonetabs-handleServerError;
+}
+
+/* Collapsible sections */
+.collapseAdvanced .headerAdvanced:click {
+    action-client: plonetabs-toggleCollapsible;
+}
+
+/* Reordering rules */
+#tabslist:load {
+    action-client: plonetabs-createSortable;
+    plonetabs-createSortable-handle: 'drag-handle';
+}
+
+#tabslist li:load {
+    action-client: plonetabs-updateSortable;
+    plonetabs-updateSortable-handle: 'drag-handle';
+}
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/actionslist.pt
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/actionslist.pt	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/actionslist.pt	(revision 540)
@@ -0,0 +1,118 @@
+<tal:tabs tal:define="test nocall:view/test;
+                      dummy python:request.RESPONSE.setHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT');
+                      category python:options.get('category', None) or 'portal_tabs';
+                      errors options/errors|python:{}"
+          tal:repeat="tab options/tabs|python:view.getPortalActions(category)">
+<li tal:define="id tab/id;
+                visible tab/visible;
+                editing tab/editing|nothing;
+                klass python:test(visible, [], ['invisible']);
+                klass python:test(editing, klass + ['editing'], klass)"
+    tal:attributes="id string:${view/prefix}${id};
+                    title tab/description;
+                    class python:' '.join(klass)">
+
+  <a class="delete" href="#">Delete</a>
+  <div class="titleWrapper kssHidden">
+    <span class="tab-title" tal:content="tab/title">Tab Name</span>
+    <span class="url-helper" tal:content="tab/url_expr">Tab Action</span>
+  </div>
+  <form class="editform"
+        method="post"
+        action="@@plonetabs-controlpanel"
+        name="edit_form"
+        tal:attributes="action string:${context/portal_url}/${attrs/action};
+                        name string:${attrs/name}_${id}">
+
+    <input type="hidden" name="orig_id" value="orig_id" tal:attributes="value id" />
+    <input type="hidden" name="category" value="current_category" tal:attributes="value category" />
+    <input type="hidden" name="form.submitted:boolean" value="True" />
+
+    <div class="bridge">
+      <img class="drag-handle" src="++resource++drag.gif" alt="" height="11" width="11" />
+      <input type="checkbox" class="visibility" value="1" name="visible" title="visibility"
+             tal:define="name string:${attrs/name}_${id};
+                         submitted python:test(request.get('form.submitted','') and request.get('orig_id','')==id, True, False)"
+             tal:attributes="name name;
+                             checked python:test(submitted, test(request.form.get(name, False), 'checked', None), test(visible, 'checked', None))"/>
+    </div>
+
+    <div class="edit-fields-wrapper">
+
+      <dl class="edit-field-name"
+          tal:define="name string:title_${id};
+                      error python:errors.get(name, '');
+                      tab_title tab/title"
+          tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+        <dt><label tal:attributes="for name">Name</label></dt>
+        <dd>
+          <span class="error-container" tal:content="error">Validation error output</span>
+          <input type="text" value="" name="title"
+                 tal:attributes="value python:test(request.get(name, []) != [], request.get(name), tab_title);
+                                 name name;
+                                 id name" /></dd>
+      </dl>
+
+      <dl class="collapseAdvanced collapsedBlock">
+        <dt class="headerAdvanced">Advanced</dt>
+        <dd class="contentAdvanced">
+          <dl class="edit-field-action"
+              tal:define="name string:url_expr_${id};
+                          error python:errors.get(name, '');
+                          tab_url tab/url_expr"
+              tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+            <dt><label tal:attributes="for name">URL (Expression)</label></dt>
+            <dd>
+              <span class="error-container" tal:content="error">Validation error output</span>
+              <input type="text" value="" name="url_expr"     size="30"
+                     tal:attributes="value python:test(request.get(name, []) != [], request.get(name), tab_url);
+                                     name name;
+                                     id name" /></dd>
+          </dl>
+          <dl class="edit-field-id"
+              tal:define="name string:id_${id};
+                          error python:errors.get(name, '');
+                          tab_id tab/id"
+              tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+            <dt><label tal:attributes="for name">Id</label></dt>
+            <dd>
+              <span class="error-container" tal:content="error">Validation error output</span>
+              <input type="text" value="" name="id"
+                     tal:attributes="value python:test(request.get(name, []) != [], request.get(name), tab_id);
+                                     name name;
+                                     id name" /></dd>
+          </dl>
+          <dl class="edit-field-condition"
+              tal:define="name string:available_expr_${id};
+                          error python:errors.get(name, '');
+                          tab_cond tab/available_expr"
+              tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+            <dt><label tal:attributes="for name">Condition (Expression)</label></dt>
+            <dd>
+              <span class="error-container" tal:content="error">Validation error output</span>
+              <input type="text" value="" name="available_expr"  size="30"
+                     tal:attributes="value python:test(request.get(name, []) != [], request.get(name), tab_cond);
+                                     name name;
+                                     id name" /></dd>
+          </dl>
+          <div class="visualClear"><!-- --></div>
+        </dd>
+      </dl>
+
+      <div class="edit-controls">
+        <input type="submit" class="editsave context"   i18n:attributes="value" name="edit.save" value="Save" />
+        <input type="submit" class="editcancel standalone" i18n:attributes="value" name="edit.cancel" value="Cancel" />
+        <input type="reset"  class="editreset kssHidden context"  i18n:attributes="value" name="edit.reset" value="Reset" />
+        <input type="submit" class="editdelete kssHidden context"  i18n:attributes="value" name="edit.delete" value="Delete" />
+        <input type="submit" class="editmoveup kssHidden context"  i18n:attributes="value" name="edit.moveup" value="Move Up"
+               tal:condition="not:repeat/tab/start" />
+        <input type="submit"  class="editmovedown kssHidden context"  i18n:attributes="value" name="edit.movedown" value="Move Down"
+               tal:condition="not:repeat/tab/end" />
+      </div>
+
+    </div>
+
+  </form>
+
+</li>
+</tal:tabs>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/autogenerated.pt
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/autogenerated.pt	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/autogenerated.pt	(revision 540)
@@ -0,0 +1,59 @@
+<metal:header_macro use-macro="context/@@plonetabs-header-macro/macros/header" />
+
+  <div id="autogeneration_section"
+       tal:define="category options/category;
+                   errors options/errors|nothing;
+                   test nocall:view/test">
+
+    <form name="generated_tabs_form"
+          action="@@plonetabs-controlpanel"
+          method="post"
+          tal:attributes="action string:${portal_url}/${attrs/action}"
+          tal:condition="python:category == 'portal_tabs'">
+
+      <input type="hidden" name="form.submitted:boolean" value="True" />
+
+      <div class="field"
+           tal:define="generated_tabs request/generated_tabs|view/isGeneratedTabs;
+                       error errors/generated_tabs|nothing"
+           tal:attributes="class python:test(error, 'field error', 'field')">
+        <input type="checkbox"
+               id="generated_tabs"
+               name="generated_tabs"
+               value="1"
+               tal:attributes="checked python:test(generated_tabs, 'checked', None)" />
+        <label for="generated_tabs" i18n:translate="label_generated_tabs_enable">Automatically generate tabs</label>
+        <div class="formHelp" i18n:translate="help_generated_tabs">
+          By default, all folders created at the root level will have global section
+          navigation generated. You can turn this off if you prefer manually constructing
+          this part of the navigation.
+        </div>
+      </div>
+
+      <div class="field"
+           tal:define="nonfolderish_tabs request/nonfolderish_tabs|view/isNotFoldersGenerated;
+                       error errors/nonfolderish_tabs|nothing"
+           tal:attributes="class python:test(error, 'field error', 'field')">
+        <input type="checkbox"
+               id="nonfolderish_tabs"
+               name="nonfolderish_tabs"
+               value="1"
+               tal:attributes="checked python:test(nonfolderish_tabs, 'checked', None)" />
+        <label for="nonfolderish_tabs" i18n:translate="label_nonfolderish_tabs_enable">Generate tabs for items other than folders</label>
+        <div class="formHelp" i18n:translate="help_nonfolderish_tabs">
+          By default, any content item in the root of the portal will be shown as
+          a global section. If you turn this option off, only folders will be shown.
+          This only has an effect if "Automatically generate tabs" is enabled
+        </div>
+      </div>
+
+      <ul class="csshover" id="roottabs"
+          tal:content="structure view/getGeneratedTabs" />
+
+      <div id="autogeneration_controls">
+          <input class="submit-button context" type="submit" name="autogenerated.save" value="Save" />
+      </div>
+
+    </form>
+
+  </div>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/autogeneratedlist.pt
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/autogeneratedlist.pt	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/autogeneratedlist.pt	(revision 540)
@@ -0,0 +1,18 @@
+<metal:header_macro use-macro="context/@@plonetabs-header-macro/macros/header" />
+
+<tal:tabs tal:define="dummy python:request.RESPONSE.setHeader('Expires', 'Mon, 26 Jul 1996 05:00:00 GMT')"
+          tal:repeat="tab view/getRootTabs">
+<li tal:define="id tab/id;
+                visible not: tab/exclude_from_nav"
+    tal:attributes="id string:roottabs_${id};
+                    title tab/description|nothing;
+                    class python:view.test(visible, '', 'invisible')">
+  <div class="bridge">
+    <input type="checkbox" class="visibility" value="1" name="id"  title="visibility"
+           tal:attributes="name id;
+                           checked python:view.test(visible, 'checked', None)" />
+  </div>
+  <span class="tab-title"  tal:content="tab/name">Tab Name</span>
+  <span class="url-helper" tal:content="tab/url">Tab Action</span>
+</li>
+</tal:tabs>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/plonetabs.pt
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/plonetabs.pt	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/plonetabs.pt	(revision 540)
@@ -0,0 +1,154 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      xml:lang="en" lang="en"
+      metal:use-macro="context/prefs_main_template/macros/master"
+      i18n:domain="quintagroup.plonetabs">
+
+<metal:block metal:fill-slot="top_slot"
+             tal:define="dummy python:request.set('disable_border', 1)" />
+
+<body>
+
+<div metal:fill-slot="prefs_configlet_main"
+     tal:define="test nocall:view/test;
+                 category here/REQUEST/category|nothing;
+                 category python:test(category and category != '', category, 'portal_tabs');
+                 errors options/errors|nothing">
+
+<div id="plonetabs_form">
+
+<div id="app" class="qPloneTabs">
+
+  <h1 id="plonetabs-form-title"
+      class="documentFirstHeading"
+      i18n:translate=""
+      tal:content="python:view.getPageTitle(category)">Plone Tabs</h1>
+
+  <a href=""
+     class="link-parent"
+     tal:attributes="href string: $portal_url/plone_control_panel"
+     i18n:domain="plone"
+     i18n:translate="label_up_to_plone_setup">
+    Up to Site Setup
+  </a>
+
+  <div i18n:translate="description_plonetabs">
+    <form id="selectcategory_form"
+          name="selectcategory_form"
+          method="post"
+          action="@@plonetabs-controlpanel"
+          tal:attributes="action string:${portal_url}/${attrs/action}">
+      <div class="field"
+           tal:define="error errors/select_category|nothing"
+           tal:attributes="class python:test(error, 'field error', 'field')">
+        <div tal:replace="error">Error output</div>
+        <label for="select_category" i18n:translate="label_select_category">Select category to manage</label>
+        <select name="category"
+                id="select_category"
+                tal:define="categories view/getCategories">
+          <option value="current_category" selected="selected"
+                  tal:attributes="value category"
+                  tal:content="category" />
+          <tal:options repeat="cat categories">
+            <option tal:attributes="value cat"
+                    tal:content="cat"
+                    tal:condition="python: cat != category" />
+          </tal:options>
+        </select>
+        <input class="change-button kssHidden" type="submit" name="category.change" value="Change" />
+      </div>
+    </form>
+  </div>
+
+  <ul class="csshover"
+      id="tabslist"
+      tal:content="structure python:view.getActionsList(category=category, errors=errors)" />
+
+  <form id="addaction"
+        name="addaction_form"
+        method="post"
+        action="@@plonetabs-controlpanel"
+        tal:attributes="action string:${portal_url}/${attrs/action}">
+    <input type="hidden" name="form.submitted:boolean" value="True" />
+    <input type="hidden" name="category" value="category" tal:attributes="value category" />
+    <dl class="field-visible bridge">
+      <dt>
+        <label for="actvisible">Visible</label>
+      </dt>
+      <dd>
+        <input id="actvisible" type="checkbox" value="1" checked="checked" title="visibility" name="visible"
+               tal:attributes="checked request/visible|string:checked" />
+      </dd>
+    </dl>
+    <dl class="field-name"
+        tal:define="error errors/title|nothing"
+        tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+      <dt>
+        <label for="actname">Name</label>
+      </dt>
+      <dd>
+        <span class="error-container" tal:content="error">Validation error output</span>
+        <input id="actname" type="text" value="" name="title"
+               tal:attributes="value request/title|nothing"/>
+      </dd>
+    </dl>
+    <dl class="field-action"
+        tal:define="error errors/url_expr|nothing"
+        tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+      <dt>
+        <label for="actaction">URL (Expression)</label>
+      </dt>
+      <dd>
+        <span class="error-container" tal:content="error">Validation error output</span>
+        <input id="actaction" type="text" value="" size="30" name="url_expr"
+               tal:attributes="value request/url_expr|nothing"/>
+      </dd>
+    </dl>
+    <dl class="collapseAdvanced expandedBlock">
+      <dt class="headerAdvanced">Advanced</dt>
+      <dd class="contentAdvanced">
+        <dl class="field-id"
+            tal:define="error errors/id|nothing"
+            tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+          <dt>
+            <label for="actid">Id</label>
+          </dt>
+          <dd>
+            <span class="error-container" tal:content="error">Validation error output</span>
+            <input id="actid" type="text" value="" name="id"
+                   tal:attributes="value request/id|nothing" />
+          </dd>
+        </dl>
+        <dl class="field-condition"
+            tal:define="error errors/available_expr|nothing"
+            tal:attributes="class python:test(error, '%s error' % attrs['class'], attrs['class'])">
+          <dt>
+            <label for="actcondition">Condition (Expression)</label>
+          </dt>
+          <dd>
+            <span class="error-container" tal:content="error">Validation error output</span>
+            <input id="actcondition" type="text" value="" size="30" name="available_expr"
+                   tal:attributes="value request/available_expr|nothing" />
+          </dd>
+        </dl>
+        <div class="visualClear"><!-- --></div>
+      </dd>
+    </dl>
+    <div class="add-controls">
+      <input type="submit" id="buttonadd" class="context" i18n:attributes="value" name="add.add" value="Add" />
+      <input type="submit" id="buttoncancel" class="standalone" i18n:attributes="value" name="add.cancel" value="Cancel" />
+    </div>
+  </form>
+
+  <tal:autogenerated_section tal:replace="structure python:view.getAutoGenereatedSection(cat_name=category, errors=errors)" />
+
+</div>
+
+</div>
+
+</div>
+
+</body>
+</html>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/plonetabs_header_macro.pt
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/plonetabs_header_macro.pt	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/plonetabs_header_macro.pt	(revision 540)
@@ -0,0 +1,12 @@
+<metal:header_macro define-macro="header">
+
+<metal:block use-macro="here/global_defines/macros/defines" />
+
+<tal:cache tal:define="lang language;
+                       charset site_properties/default_charset|string:utf-8">
+  <metal:cache use-macro="here/global_cache_settings/macros/cacheheaders">
+    Get the global cache headers located in global_cache_settings.
+  </metal:cache>
+</tal:cache>
+
+</metal:header_macro>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/sections.pt
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/sections.pt	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/browser/templates/sections.pt	(revision 540)
@@ -0,0 +1,15 @@
+<metal:header_macro use-macro="here/@@plonetabs-header-macro/macros/header" />
+
+<tal:tabs i18n:domain="plone">
+
+    <ul id="portal-globalnav">
+        <tal:tabs tal:repeat="tab view/portal_tabs"><li tal:attributes="id string:portaltab-${tab/id};
+                            class python:view.selected_portal_tab==tab['id'] and 'selected' or 'plain'">
+            <a href="" 
+               tal:content="tab/name"
+               tal:attributes="href tab/url;
+                               title tab/description|nothing">
+            Tab Name
+            </a></li></tal:tabs>
+    </ul>
+</tal:tabs>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/config.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/config.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/config.py	(revision 540)
@@ -0,0 +1,2 @@
+PROPERTY_SHEET = "tabs_properties"
+FIELD_NAME     = "titles"
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/configure.zcml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/configure.zcml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/configure.zcml	(revision 540)
@@ -0,0 +1,15 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five"
+    i18n_domain="quintagroup.plonetabs">
+
+    <!-- Set up as a Zope2 product -->
+    <five:registerPackage package="." initialize=".initialize" />
+    
+    <include file="profiles.zcml" />
+    
+    <include package=".browser" />
+    
+    <include package=".plugins" />
+    
+</configure>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/__init__.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/__init__.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/__init__.py	(revision 540)
@@ -0,0 +1,1 @@
+# I'm a package
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/configure.zcml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/configure.zcml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/configure.zcml	(revision 540)
@@ -0,0 +1,69 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+     xmlns:browser="http://namespaces.zope.org/browser"
+     xmlns:kss="http://namespaces.zope.org/kss"
+     >
+     
+    <!-- plugin registration -->    
+
+    <!-- kss client actions -->
+    
+    <kss:action
+        name="plonetabs-redirectTo"
+        command_factory="selector"
+        jsfile="plugin.js"
+        params_optional="protocol host pathname search searchparams searchvalues hash"
+        />
+    
+    <kss:action
+        name="plonetabs-toggleCollapsible"
+        command_factory="selector"
+        jsfile="plugin.js"
+        params_optional="collapsed expanded collapse"
+        />
+
+    <kss:action
+        name="plonetabs-handleServerError"
+        jsfile="plugin.js"
+        params_optional="message"
+        />
+    
+    <kss:action
+        name="plonetabs-resetForm"
+        command_factory="selector"
+        jsfile="plugin.js"
+        />
+
+    <kss:action
+        name="plonetabs-blur"
+        command_factory="selector"
+        jsfile="plugin.js"
+        />
+    
+    <kss:action
+        name="plonetabs-generateId"
+        command_factory="selector"
+        jsfile="plugin.js"
+        params_mandatory="target var_name"
+        />
+    
+    <kss:action
+        name="plonetabs-createSortable"
+        command_factory="selector"
+        jsfile="plugin.js"
+        />
+    
+    <kss:action
+        name="plonetabs-updateSortable"
+        command_factory="selector"
+        jsfile="plugin.js"
+        />
+    
+    <kss:action
+        name="plonetabs-replaceOrInsert"
+        command_factory="selector"
+        jsfile="plugin.js"
+        params_mandatory="selector html"
+        params_optional="alternativeHTML selectorType position positionSelector positionSelectorType withKssSetup"
+        />
+    
+</configure>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/plugin.js
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/plugin.js	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/plugins/plugin.js	(revision 540)
@@ -0,0 +1,221 @@
+/* KSS plugins for plonetabs */
+
+kukit.actionsGlobalRegistry.register('plonetabs-redirectTo', function(oper) {
+;;; oper.componentName = '[plonetabs-redirectTo] action';
+    var wl = window.location;
+    oper.evaluateParameters([], {'protocol'     : wl.protocol,
+                                 'host'         : wl.host,
+                                 'pathname'     : wl.pathname,
+                                 'search'       : wl.search,
+                                 'searchparams' : '',
+                                 'searchvalues' : '',
+                                 'hash'         : wl.hash});
+
+    // normalize parameters
+    var protocol = oper.parms.protocol + (oper.parms.protocol[oper.parms.protocol.length - 1] == ':') ?  '' : ':';
+    var host = oper.parms.host;
+    var pathname = oper.parms.pathname;
+    if ((params = oper.parms.searchparams) && (values = oper.parms.searchvalues)) {
+        search = '?';
+        params = params.split(',');
+        values = values.split(',');
+        for (var i = 0; i < params.length; i++) {
+            search += params[i] + '=' + values[i] + '&';
+        }
+        search = (search.slice(search.length - 1) == '&') ? search.slice(0, search.length - 1) : search;
+    } else {
+        search = oper.parms.search;
+        search = (search && search.substr(0, 1) == '?') ? search : (search ? '?' + search : '');
+    }
+    var hash = (oper.parms.hash.length > 1) ? ((oper.parms.hash.substr(0, 1) == '#' ? '' : '#') + oper.parms.hash) : '';
+
+    url = protocol + '//' + host + pathname + search + hash;
+    window.location.replace(url);
+
+});
+
+kukit.commandsGlobalRegistry.registerFromAction('plonetabs-redirectTo', kukit.cr.makeSelectorCommand);
+
+
+kukit.actionsGlobalRegistry.register('plonetabs-toggleCollapsible', function(oper) {
+;;; oper.componentName = '[plonetabs-toggleCollapsible] action';
+    oper.evaluateParameters([], {'collapsed' : 'collapsedBlock',
+                                 'expanded' : 'expandedBlock',
+                                 'collapse': 'none'});
+
+    var node = oper.node.parentNode;  // collapsible section
+
+    if (oper.parms.collapse != 'none') {
+        if (oper.parms.collapse == 'true') {
+            removeClassName(node, oper.parms.expanded);
+            addClassName(node, oper.parms.collapsed);
+        } else {
+            removeClassName(node, oper.parms.collapsed);
+            addClassName(node, oper.parms.expanded);
+        }
+    } else {
+        if (hasClassName(node, oper.parms.collapsed)) {
+            removeClassName(node, oper.parms.collapsed);
+            addClassName(node, oper.parms.expanded);
+        } else {
+            removeClassName(node, oper.parms.expanded);
+            addClassName(node, oper.parms.collapsed);
+        }
+    }
+
+});
+
+kukit.commandsGlobalRegistry.registerFromAction('plonetabs-toggleCollapsible', kukit.cr.makeSelectorCommand);
+
+
+kukit.actionsGlobalRegistry.register('plonetabs-resetForm', function(oper) {
+;;; oper.componentName = '[plonetabs-resetForm] action';
+    oper.evaluateParameters([], {});
+
+    if (typeof(oper.node.reset) == 'function') {
+        oper.node.reset();
+    } else {
+        kukit.logWarning('plonetabs-resetForm: reset could only be executed on form element');
+    }
+
+});
+
+kukit.commandsGlobalRegistry.registerFromAction('plonetabs-resetForm', kukit.cr.makeSelectorCommand);
+
+function blur(node) {
+    tagName = node.tagName.toLowerCase();
+    if ((tagName == 'input') || (tagName == 'select')
+       || (tagName == 'textarea')) {
+        node.blur();
+;;;} else {
+;;;    kukit.logWarning('Blur on node that cannot be blured!');
+    };
+};
+
+kukit.actionsGlobalRegistry.register('plonetabs-blur', function(oper) {
+;;; oper.componentName = '[plonetabs-blur] action';
+    // TODO get rid of none
+    oper.evaluateParameters([], {'none': false});
+    blur(oper.node);
+
+});
+
+kukit.commandsGlobalRegistry.registerFromAction('plonetabs-blur', kukit.cr.makeSelectorCommand);
+
+kukit.actionsGlobalRegistry.register('plonetabs-handleServerError', function(oper) {
+    oper.componentName = '[plonetabs-handleServerError] action';
+    oper.evaluateParameters([], {'message' : kukit.E});
+    alert(oper.parms.message);
+});
+
+
+var PLONETABS_ADD_PATTERN = new RegExp('[^a-zA-Z0-9-_~,.\\$\\(\\)# ]','g');
+
+kukit.actionsGlobalRegistry.register('plonetabs-generateId', function(oper) {
+    oper.componentName = '[plonetabs-generateId] action';
+    oper.evaluateParameters(['target', 'var_name'], {});
+
+    var source = oper.node;
+    var var_name = oper.parms.var_name;
+    var initialValue = typeof(kukit.engine.stateVariables[var_name]) != 'undefined' ? kukit.engine.stateVariables[var_name] : '';
+    var target = document.getElementById(oper.parms.target);
+
+    if (target == null) {
+        kukit.logWarning('plonetabs-generateId: target element ("' + oper.parms.target + '") not found');
+        return ;
+    }
+
+    if (target.value == initialValue.replace(PLONETABS_ADD_PATTERN, '')) {
+        target.value = source.value.replace(PLONETABS_ADD_PATTERN, '');
+    }
+    kukit.engine.stateVariables[var_name] = source.value;
+
+});
+
+function plonetabs_notifySortableUpdate(element, oper) {
+    var draggables = element.getElementsByTagName('LI');
+    var ids = [];
+    for (var i = 0, o; o = draggables[i]; i++) {
+        ids.push(o.id);
+    }
+    oper.parms = {'actions': ids.join('&'), 'cat_name': kukit.engine.stateVariables['plonetabs-category']};
+    oper.executeServerAction('plonetabs-orderActions');
+}
+
+kukit.actionsGlobalRegistry.register('plonetabs-createSortable', function(oper) {
+    oper.componentName = '[plonetabs-createSortable] action';
+    oper.evaluateParameters([], {}, '', true);
+    var parms = oper.clone().parms;
+
+    var new_oper = oper.clone();
+    parms['onUpdate'] = function(element){plonetabs_notifySortableUpdate(element, new_oper);};
+    Sortable.create(oper.node, parms);
+});
+
+kukit.actionsGlobalRegistry.register('plonetabs-updateSortable', function(oper) {
+    oper.componentName = '[plonetabs-updateSortable] action';
+    oper.evaluateParameters([], {}, '', true);
+    var parms = oper.clone().parms;
+    var node = oper.node;
+    var sort_list = node.parentNode;
+    var options_ = Sortable.sortables[sort_list.id];
+
+    if (typeof(options_) != 'undefined') {
+        // check whether node element isn't already registered as draggables
+        for (var i = 0, drag; drag = options_.draggables[i]; i++) {
+            if (node == drag.element) {
+                return false;
+            }
+        }
+        // destroy sortable list
+        Sortable.destroy(sort_list.id);
+    }
+
+    var new_oper = oper.clone();
+    parms['onUpdate'] = function(element){plonetabs_notifySortableUpdate(element, new_oper);};
+    Sortable.create(sort_list, parms);
+});
+
+kukit.commandsGlobalRegistry.registerFromAction('plonetabs-updateSortable', kukit.cr.makeSelectorCommand);
+
+kukit.actionsGlobalRegistry.register('plonetabs-replaceOrInsert', function(oper) {
+    oper.componentName = '[plonetabs-replaceOrInsert] action';
+    var defaultSelectorType = kukit.selectorTypesGlobalRegistry.defaultSelectorType
+    oper.evaluateParameters(['selector', 'html'], {'alternativeHTML': '',
+                                                   'selectorType': defaultSelectorType,
+                                                   'position': 'last', // can be one of the following: first, last, after, before
+                                                   'positionSelector': '', // work together with position=after|before
+                                                   'positionSelectorType': defaultSelectorType,
+                                                   'withKssSetup': true});
+    oper.evalBool('withKssSetup');
+    var parentNode = oper.node;
+    var nodes = kukit.selectorTypesGlobalRegistry.get(oper.parms.selectorType)(oper.parms.selector, parentNode);
+    if (nodes.length > 0) {
+        var content = oper.parms.html;
+        var new_node = nodes[0];
+        var action_ = 'replaceHTML';
+    } else {
+        var content = oper.parms.alternativeHTML ? oper.parms.alternativeHTML : oper.parms.html;
+        var action_ = 'insertHTMLAsLastChild';
+        var new_node = parentNode;
+        var position = oper.parms.position;
+        if (position == 'first') {
+            action_ = 'insertHTMLAsFirstChild';
+        } else if (position == 'after' || position == 'before') {
+            var posSelector = kukit.selectorTypesGlobalRegistry.get(oper.parms.positionSelectorType);
+            nodes = posSelector(oper.parms.positionSelector, new_node);
+            if (nodes.length > 0) {
+                new_node = nodes[0];
+                action_ = (position == 'after') ? 'insertHTMLAfter' : 'insertHTMLBefore';
+            }
+        }
+    }
+    var new_oper = new kukit.op.Oper({'node': new_node,
+                                      'parms': {'html': content,
+                                                'withKssSetup': oper.parms.withKssSetup}});
+    kukit.actionsGlobalRegistry.get(action_)(new_oper);
+});
+
+kukit.commandsGlobalRegistry.registerFromAction('plonetabs-replaceOrInsert', kukit.cr.makeSelectorCommand);
+
+
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles.zcml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles.zcml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles.zcml	(revision 540)
@@ -0,0 +1,14 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+    i18n_domain="quintagroup.plonetabs">
+    
+    <genericsetup:registerProfile
+       name="quintagroup.plonetabs"
+       title="Plone Tabs"
+       directory="profiles/default"
+       description="Quintagroup Plone Tabs for manage portal_actions tool"
+       provides="Products.GenericSetup.interfaces.EXTENSION"
+       />
+    
+</configure>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/actionicons.xml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/actionicons.xml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/actionicons.xml	(revision 540)
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<action-icons xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+    i18n:domain="plone">
+ <action-icon category="controlpanel" action_id="plonetabs"
+              title="Plone Tabs" priority="0"
+              icon_expr="++resource++plonetabs.gif"
+              i18n:attributes="title"/>
+</action-icons>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/controlpanel.xml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/controlpanel.xml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/controlpanel.xml	(revision 540)
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<object name="portal_controlpanel" meta_type="Plone Control Panel Tool"
+    xmlns:i18n="http://xml.zope.org/namespaces/i18n" i18n:domain="plone">
+ <configlet title="Plone Tabs" action_id="plonetabs"
+    appId="quintagroup.plonetabs" category="Products" condition_expr=""
+    url_expr="string:${portal_url}/@@plonetabs-controlpanel"
+    visible="True" i18n:attributes="title">
+  <permission>Manage portal</permission>
+ </configlet>
+</object>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/cssregistry.xml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/cssregistry.xml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/cssregistry.xml	(revision 540)
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="portal_css" meta_type="Stylesheets Registry">
+ <stylesheet title="Plone Tabs" cacheable="True" compression="safe" cookable="True"
+    enabled="1" expression="python:request['URL'].find('/@@plonetabs-controlpanel')>0"
+    id="++resource++plonetabs.css" media="screen" rel="stylesheet" rendering="import"/>
+</object>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/jsregistry.xml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/jsregistry.xml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/jsregistry.xml	(revision 540)
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<object name="portal_javascripts" meta_type="JavaScripts Registry">
+ <javascript cacheable="True" compression="safe" cookable="True" enabled="True"
+    expression="python:request['URL'].find('/@@plonetabs-controlpanel')>0"
+    id="++resource++prototype.js" inline="False" insert-bottom="True" />
+ <javascript cacheable="True" compression="safe" cookable="True" enabled="True"
+    expression="python:request['URL'].find('/@@plonetabs-controlpanel')>0"
+    id="++resource++pt_effects.js" inline="False"
+    insert-after="++resource++prototype.js"/>
+ <javascript cacheable="True" compression="safe" cookable="True" enabled="True"
+    expression="python:request['URL'].find('/@@plonetabs-controlpanel')>0"
+    id="++resource++sa_dragdrop.js" inline="False"
+    insert-after="++resource++pt_effects.js" />
+ </object>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/kssregistry.xml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/kssregistry.xml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/kssregistry.xml	(revision 540)
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<object name="portal_kss" meta_type="KSS Registry">
+ <kineticstylesheet cacheable="True" compression="safe" cookable="True"
+    enabled="1" expression="python:request['URL'].find('/@@plonetabs-controlpanel')>0" id="++resource++plonetabs.kss"/>
+</object>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/propertiestool.xml
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/propertiestool.xml	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/profiles/default/propertiestool.xml	(revision 540)
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<object name="portal_properties" meta_type="Plone Properties Tool">
+ <object name="tabs_properties" meta_type="Portal Tabs Properties">
+  <property name="titles" type="lines">
+   <element value="portal_tabs|Portal Tabs Configuration"/>
+   <element value="portal_footer|Portal Footer Configuration"/>
+  </property>
+ </object>
+</object>
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/tests.py
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/tests.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/tests.py	(revision 540)
@@ -0,0 +1,54 @@
+import unittest
+
+from zope.testing import doctestunit
+from zope.component import testing
+from Testing import ZopeTestCase as ztc
+
+from Products.Five import zcml
+from Products.Five import fiveconfigure
+from Products.PloneTestCase import PloneTestCase as ptc
+from Products.PloneTestCase.layer import PloneSite
+ptc.setupPloneSite()
+
+import quintagroup.plonetabs
+
+class TestCase(ptc.PloneTestCase):
+    class layer(PloneSite):
+        @classmethod
+        def setUp(cls):
+            fiveconfigure.debug_mode = True
+            zcml.load_config('configure.zcml',
+                             quintagroup.plonetabs)
+            fiveconfigure.debug_mode = False
+
+        @classmethod
+        def tearDown(cls):
+            pass
+
+
+def test_suite():
+    return unittest.TestSuite([
+
+        # Unit tests
+        #doctestunit.DocFileSuite(
+        #    'README.txt', package='quintagroup.plonetabs',
+        #    setUp=testing.setUp, tearDown=testing.tearDown),
+
+        #doctestunit.DocTestSuite(
+        #    module='quintagroup.plonetabs.mymodule',
+        #    setUp=testing.setUp, tearDown=testing.tearDown),
+
+
+        # Integration tests that use PloneTestCase
+        #ztc.ZopeDocFileSuite(
+        #    'README.txt', package='quintagroup.plonetabs',
+        #    test_class=TestCase),
+
+        #ztc.FunctionalDocFileSuite(
+        #    'browser.txt', package='quintagroup.plonetabs',
+        #    test_class=TestCase),
+
+        ])
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
Index: /quintagroup.plonetabs/trunk/quintagroup/plonetabs/version.txt
===================================================================
--- /quintagroup.plonetabs/trunk/quintagroup/plonetabs/version.txt	(revision 540)
+++ /quintagroup.plonetabs/trunk/quintagroup/plonetabs/version.txt	(revision 540)
@@ -0,0 +1,1 @@
+0.5
Index: /quintagroup.plonetabs/trunk/setup.cfg
===================================================================
--- /quintagroup.plonetabs/trunk/setup.cfg	(revision 540)
+++ /quintagroup.plonetabs/trunk/setup.cfg	(revision 540)
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
Index: /quintagroup.plonetabs/trunk/setup.py
===================================================================
--- /quintagroup.plonetabs/trunk/setup.py	(revision 540)
+++ /quintagroup.plonetabs/trunk/setup.py	(revision 540)
@@ -0,0 +1,34 @@
+from setuptools import setup, find_packages
+
+version = '0.5'
+
+setup(name='quintagroup.plonetabs',
+      version=version,
+      description="Quintagroup Plone Tabs",
+      long_description="""\
+The product allows Plone websites to manage portal_actions tool from portal_control_panel configlet.""",
+      # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      classifiers=[
+        "Framework :: Plone",
+        "Framework :: Zope2",
+        "Framework :: Zope3",
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+      keywords='quintagroup plonetabs',
+      author='(c) "Quintagroup": http://quintagroup.com/ , 2008',
+      author_email='info@quintagroup.com',
+      url='http://projects.quintagroup.com/products/wiki/qPloneTabs',
+      license='GPL',
+      packages=find_packages(exclude=['ez_setup']),
+      namespace_packages=['quintagroup'],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          'setuptools',
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )
