Repository: scipion-em/scipion-pyworkflow (language: python)

Issue:
Templates from wh
lists templates from workflow hub site


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bfbbb3ad5..c78ef8ba1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,17 +18,17 @@ jobs:
         shell: bash -el {0}
     strategy:
       fail-fast: false
-      max-parallel: 6
+      max-parallel: 5
       matrix:
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: 3.x
     - name: Create a conda env called test with a specific python version
-      uses: conda-incubator/setup-miniconda@v2
+      uses: conda-incubator/setup-miniconda@v3
       with:
         miniconda-version: "latest"
         activate-environment: test
@@ -41,4 +41,4 @@ jobs:
         export PACKAGE_VERSION=$(python -c "import $FOLDER_WITH_VERSION; print('v'+$FOLDER_WITH_VERSION.__version__);")
         echo Built $FOLDER_WITH_VERSION version $PACKAGE_VERSION
         pip install git+https://github.com/scipion-em/scipion-em@devel#egg=scipion-em
-        pip install git+https://github.com/scipion-em/scipion-app@devel#egg=scipion-app
+        pip install git+https://github.com/scipion-em/scipion-app@update-req#egg=scipion-app
diff --git a/.github/workflows/publish_and_tag.yml b/.github/workflows/publish_and_tag.yml
index 37dfcd276..0f8238b5f 100644
--- a/.github/workflows/publish_and_tag.yml
+++ b/.github/workflows/publish_and_tag.yml
@@ -15,25 +15,24 @@ env:
 # A workflow run is made up of one or more jobs that can run sequentially or in parallel
 jobs:
   deploy:
-
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v5
       with:
-        python-version: '3.x'
+        python-version: '3.12'
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install setuptools wheel twine
+        pip install setuptools wheel twine build
+        pip install -r requirements.txt
     - name: Build and publish
       env:
         TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
         TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
       run: |
-        python setup.py sdist
+        python -m build --no-isolation
         twine upload dist/* -c "${{ secrets.PYPI_COMMENT }}"
     - name: Get version and tag
       run: |
diff --git a/.sonarcloud.properties b/.sonarcloud.properties
new file mode 100644
index 000000000..85985f0bd
--- /dev/null
+++ b/.sonarcloud.properties
@@ -0,0 +1,1 @@
+sonar.python.version=3.8, 3.9, 3.10, 3.11, 3.12
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b1836f5ae..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-language: python
-os: linux
-dist: bionic
-python: 3.8
-git:
-    depth: false # to get blame information for SonarCloud
-addons:
-  sonarcloud:
-# See https://docs.travis-ci.com/user/sonarcloud/
-    organization: "scipion-em"
-    token: $SONAR_TOKEN
-
-script:
- - sonar-scanner
\ No newline at end of file
diff --git a/CHANGES.txt b/CHANGES.txt
index 8fd136d3b..b23423556 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,33 @@
+V3.7.0
+users:
+  - Add protocol is an icon.
+  - Right clik on canvas offers to add a protocol on the clicked location.
+  - Scipion log is a project.log (only for project events)
+  - Variables window saves non default variables changed (bug)
+  - Queue interoperability review:
+    - jobs cancelled in the queue are detected in scipion
+    - job and pids handling improved, pids = 0 when use queue for protocol rely on jobid
+    - Change Protocols Lock to Rlocks objects to avoid concurrency when updating the protocols jobid
+  - add information in the protocol log regarding execution type (pid or jobid)f
+  - Streaming protocols based on new streaming (generation steps) are resumable
+  - Avoiding a double process when launching a workflow using the protocol form
+  - pyworkflowtest tests fixed
+  - GUI will detect when protocols processes have been killed(cancelled) in all cases?
+  - fix outdated pyworkflow scripts
+  - scipion3 template (template manager) lists workflows at workflowhub.eu from "Scipion CNB" organization
+  - Join heterogeneous sets are possible for tomography objects TS and tomograms.
+developers:
+  - Canvas has a testing method: _drawGrid to draw a grid on the canvas (unused now)
+  - String.toSeconds:converts to seconds any "duration" string like "1h 4m 34s" -> 3600 + 240 + 34.
+  - ListDialog has a refresh method to refresh the content taking into account the filter
+  - Improve the DDBB lock in the method "_store". If Protocol._lock is locked, a nullcontext is used instead of another
+    Protocol._lock context, which causes a permanent lock.
+  - XmlMapper removed.
+  - FlatMapper instanatiation centralized:
+     New variable: SCIPION_MAPPER_USE_TEMPLATE. By default uses the template as it was now. If deacivated, instantiation happens on each iteration (less performant) but dev friendly
+  - Adding a parameter to launch jobs (e.g: bash)
+  - Handling item-click event in the treeview dialog
+
 V3.6.0
 users:
   - Window to see/edit config variables. Launched from Project menu in project window.
diff --git a/MANIFEST.in b/MANIFEST.in
index 2c5f83468..80f44deae 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,11 +1,5 @@
-# Include the README
  include *.rst
-
-# Include the license file
- include LICENSE.txt
-
-# Include the requirement file
-include requirements.txt
+ include *.txt
 
 # Include image files under resources
  include pyworkflow/resources/*
diff --git a/README.rst b/README.rst
index dc16177ff..144c2055d 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,30 @@
-.. image:: https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg
-  :width: 200
-  :alt: Contributor Covenant
-  :target: https://www.contributor-covenant.org/version/2/0/code_of_conduct/ 
+.. image:: https://img.shields.io/pypi/v/scipion-pyworkflow.svg
+        :target: https://pypi.python.org/pypi/scipion-pyworkflow
+        :alt: PyPI release
+
+.. image:: https://sonarcloud.io/api/project_badges/measure?project=scipion-em_scipion-pyworkflow&metric=alert_status
+        :alt: Quality Gate Status
+        :target: https://sonarcloud.io/summary/new_code?id=scipion-em_scipion-pyworkflow
+
+.. image:: https://img.shields.io/sonar/quality_gate/scipion-em_scipion-pyworkflow?server=https%3A%2F%2Fsonarcloud.io
+        :target: https://sonarcloud.io/dashboard?id=scipion-em_scipion-pyworkflow
+        :alt: SonarCloud quality gate
+
+.. image:: https://img.shields.io/pypi/l/scipion-pyworkflow.svg
+        :target: https://pypi.python.org/pypi/scipion-pyworkflow
+        :alt: License
 
+.. image:: https://img.shields.io/pypi/pyversions/scipion-pyworkflow.svg
+        :target: https://pypi.python.org/pypi/scipion-pyworkflow
+        :alt: Supported Python versions
+
+.. image:: https://img.shields.io/pypi/dm/scipion-pyworkflow
+        :target: https://pypi.python.org/pypi/scipion-pyworkflow
+        :alt: Downloads
+
+.. image:: https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg
+        :alt: Contributor Covenant
+        :target: https://www.contributor-covenant.org/version/2/0/code_of_conduct/
 
 pyworkflow
 ==========
diff --git a/pyworkflow/apps/pw_protocol_list.py b/pyworkflow/apps/pw_protocol_list.py
index 0a2b9aff1..7a42468ea 100644
--- a/pyworkflow/apps/pw_protocol_list.py
+++ b/pyworkflow/apps/pw_protocol_list.py
@@ -63,15 +63,11 @@ def hasDoubleInheritance(classRef):
 if __name__ == '__main__':
     count = 0
     withDoc = '--with-doc' in sys.argv
-    asciidoc = '--asciidoc' in sys.argv
     extended = '--extended' in sys.argv
+    ai = '--ai' in sys.argv
 
     emProtocolsDict = Config.getDomain().getProtocols()
-    emCategories = [('Imports', ProtImport, []),
-                    ('Micrographs', ProtMicrographs, []),
-                    ('Particles', ProtParticles, []),
-                    ('2D', Prot2D, []),
-                    ('3D', Prot3D, [])]
+
     protDict = {}
 
     # Group protocols by package name
@@ -90,10 +86,6 @@ def hasDoubleInheritance(classRef):
             else:
                 protTuple = (k, v)
             protDict[packageName].append(protTuple)
-            for c in emCategories:
-                if issubclass(v, c[1]):
-                    c[2].append(protTuple)
-
 
     def iterGroups(protDict):
         groups = list(protDict.keys())
@@ -111,20 +103,7 @@ def printProtocols(prots):
             print("* link:%s[%s]: %s" % (k, l, doc))
 
 
-    if asciidoc:
-        print(":toc:\n:toc-placement!:\n\ntoc::[]\n")
-
-        print("\n== By Categories\n")
-        for c in emCategories:
-            print("\n=== %s\n" % c[0])
-            printProtocols(c[2])
-
-        print("\n== By Packages\n")
-        for group, prots in iterGroups(protDict):
-            print("\n=== ", group, "(%d protocols)\n" % len(prots))
-            printProtocols(prots)
-
-    elif withDoc:
+    if withDoc:
         for group, prots in iterGroups(protDict):
             print("Package: ", group, "(%d protocols)" % len(prots))
             for p in prots:
@@ -144,6 +123,18 @@ def printProtocols(prots):
                 for p in prots:
                     print(formatStr.format(group, p[0],
                                            p[1].getClassLabel(), *p[2:]))
+        elif ai:
+            for group, prots in iterGroups(protDict):
+                for protClassName, protClass in prots:
+                    print("\nThe protocol named \"%s\" can be found in the %s plugin." %(protClass._label, group))
+                    print("\"%s\" protocol help is as follows:\n %s." % (protClass._label, protClass.__doc__))
+                    instance = protClass()
+
+                    for name, param in instance._definition.iterParams():
+                        print("\"%s\" has a \"%s\" parameter that is explained as: %s" % (protClass._label, param.label, param.help))
+
+
+
         else:
             formatStr = "{:<15}\t{:<35}\t{:<35}"
             print(formatStr.format("PACKAGE", "PROTOCOL", "LABEL"))
diff --git a/pyworkflow/config.py b/pyworkflow/config.py
index 0c0af4d41..fc440f534 100644
--- a/pyworkflow/config.py
+++ b/pyworkflow/config.py
@@ -87,12 +87,17 @@ def __init__(self, name, description, source, value, default, var_type: VarTypes
         self.source = source
         self.value = value
         self.default = default
+        self.isDefault = isDefault if isDefault is not None else self._isValueDefault()
         self.var_type = var_type
-        self.isDefault = isDefault if isDefault is not None else value==default
     def setToDefault(self):
         self.isDefault=True
         self.value=self.default
 
+    def setValue(self, new_value):
+        self.value = new_value
+        self.isDefault= self._isValueDefault()
+    def _isValueDefault(self):
+        return self.value==self.default
 class VariablesRegistry:
     _variables={}
 
@@ -272,6 +277,10 @@ def join(self, *path):
 
     SCIPION_NOTES_ARGS = _get(SCIPION_NOTES_ARGS, None)
 
+    # External text editor:
+    SCIPION_TEXT_EDITOR = _get(SCIPION_TEXT_EDITOR, '',
+    "Preferred text editor executable.", caster=str)
+
     # Aspect
     SCIPION_FONT_NAME = _get('SCIPION_FONT_NAME', "Helvetica",
     "Name of the font to use in Scipion GUI. Defaults to Helvetica.")
@@ -346,6 +355,10 @@ def join(self, *path):
        2: Run a single protocol
        3: Run a sub-workflow """, caster=int)
 
+    SCIPION_MAPPER_USE_TEMPLATE = _get('SCIPION_MAPPER_USE_TEMPLATE', TRUE_STR,
+    "Set it to False to force instantiation for each item during sets iterations. Experimental. This penalize the iteration but avoids"
+    "the use of .clone() ot the items.") == TRUE_STR
+
     try:
         VIEWERS = ast.literal_eval(_get('VIEWERS', "{}", "Json string to define which viewer are the default ones per output type."))
     except Exception as e:
diff --git a/pyworkflow/constants.py b/pyworkflow/constants.py
index 5799facba..280ac9fbd 100644
--- a/pyworkflow/constants.py
+++ b/pyworkflow/constants.py
@@ -42,7 +42,7 @@
 VERSION_1_1 = '1.1.0'
 VERSION_1_2 = '1.2.0'
 VERSION_2_0 = '2.0.0'
-VERSION_3_0 = '3.6.0'
+VERSION_3_0 = '3.7.0'
 
 # For a new release, define a new constant and assign it to LAST_VERSION
 # The existing one has to be added to OLD_VERSIONS list.
@@ -98,6 +98,9 @@
 
 SCIPION_DOMAIN = 'SCIPION_DOMAIN'
 
+# Text editor:
+SCIPION_TEXT_EDITOR = 'SCIPION_TEXT_EDITOR'
+
 # Debug constants
 SCIPION_DEBUG = 'SCIPION_DEBUG'
 SCIPION_JSON_TEMPLATES = '.json.template'
diff --git a/pyworkflow/gui/canvas.py b/pyworkflow/gui/canvas.py
index d5a20b5c2..c6d48ecf5 100644
--- a/pyworkflow/gui/canvas.py
+++ b/pyworkflow/gui/canvas.py
@@ -48,7 +48,7 @@
 
 class Canvas(tk.Canvas, Scrollable):
     """Canvas to draw some objects.
-    It will really contains a Frame, a Canvas and scrollbars"""
+    It actually is a Frame, a Canvas and scrollbars"""
     _images = {}
 
     def __init__(self, parent, tooltipCallback=None, tooltipDelay=1500, **kwargs):
@@ -59,7 +59,7 @@ def __init__(self, parent, tooltipCallback=None, tooltipDelay=1500, **kwargs):
         self.lastItem = None  # Track last item selected
         self.lastPos = (0, 0)  # Track last clicked position
         self.eventPos = (0, 0)
-        self.firstPos = None  # Track first clicked position (for a drag action)
+        self.dragging = False  # Track first clicked position (for a drag action)
         self.items = {}  # Keep a dictionary with high-level items
         self.cleanSelected = True
 
@@ -101,6 +101,18 @@ def __init__(self, parent, tooltipCallback=None, tooltipDelay=1500, **kwargs):
 
         self._menu = tk.Menu(self, tearoff=0)
 
+    def _drawGrid(self):
+        """ For debugging purposes. Do not delete.
+        Draws a grid on the canvas to get an ide about where click is happening"""
+        self.update()
+        _, _, width, height = self.frame.bbox(self)
+
+        for line in range(0, width, 100):  # range(start, stop, step)
+            self.create_line([(line, 0), (line, height)], fill='black', tags='grid_line_w')
+
+        for line in range(0, height, 100):
+            self.create_line([(0, line), (width, line)], fill='black', tags='grid_line_h')
+
     def _createTooltip(self):
         """ Create a Tooltip window to display tooltips in 
         the canvas.
@@ -186,19 +198,21 @@ def _handleMouseEvent(self, event, callback):
         xc, yc = self.getCoordinates(event)
         self.lastItem = self._findItem(xc, yc)
         self.callbackResults = None
-        self.lastPos = (0, 0)
 
-        if self.lastItem is not None:
-            if callback:
-                self.callbackResults = callback(self.lastItem)
-        self.lastPos = (xc, yc)
+        if callback:
+            self.callbackResults = callback(self.lastItem, event)
 
     def onClick(self, event):
+        # On click happens completely before onDrag
         self.cleanSelected = True
         self._unpostMenu()
         self._handleMouseEvent(event, self.onClickCallback)
         if self.lastItem is None:
+            # Moving the canvas as a whole
             self.move_start(event)
+        else:
+            # Dragging a single box
+            self.captureLastPosition(event)
 
     def onControlClick(self, event):
         self.cleanSelected = False
@@ -233,7 +247,10 @@ def onRightClick(self, e=None):
                             if shortCut:
                                 label= "%s (%s)" % (label, shortCut)
 
-                    self._menu.add_command(label=label, command=a[1],
+                    def getAction(callback):
+                        return lambda: callback(e)
+
+                    self._menu.add_command(label=label, command=getAction(a[1]),
                                            image=img, compound=tk.LEFT,
                                            font=gui.getDefaultFont())
             self._menu.post(e.x_root, e.y_root)
@@ -248,6 +265,7 @@ def onDoubleClick(self, event):
     def move_start(self, event):
         # If nothing was click on ButtonPress
         if self.lastItem is None:
+            self.captureLastPosition(event)
             self.config(cursor='fleur')
             self.scan_mark(event.x, event.y)
 
@@ -256,12 +274,12 @@ def onDrag(self, event):
 
             if self.lastItem:
                 xc, yc = self.getCoordinates(event)
-                self.lastItem.move(xc - self.lastPos[0], yc - self.lastPos[1])
+                dx, dy = xc - self.lastPos[0], yc - self.lastPos[1]
+                # logger.info("Moving item %s, %s." % (dx,dy))
+                self.lastItem.move(dx, dy)
                 self.lastPos = (xc, yc)
             else:
-                if self.firstPos is None:
-                    self.firstPos = (event.x, event.y)
-                (x, y) = self.getCoordinates(event)
+                self.dragging = True
                 self.scan_dragto(event.x, event.y, gain=1)
 
         except Exception as ex:
@@ -270,13 +288,19 @@ def onDrag(self, event):
             # event and the refresh one. For now, just ignore it.
             pass
 
+    def captureLastPosition(self, event):
+        """ Captures the last position the mouse were located upon the event
+        """
+        self.lastPos = self.getCoordinates(event)
+
+
     def onButton1Release(self, event):
-        if self.firstPos is not None:
+        if self.dragging:
             # Failing in "data" view.
             # TODO: This is not fully implemented
             # self.onAreaSelected(self.firstPos[0], self.firstPos[1], event.x, event.y)
 
-            self.firstPos = None
+            self.dragging = False
 
         self.config(cursor='arrow')
 
diff --git a/pyworkflow/gui/dialog.py b/pyworkflow/gui/dialog.py
index 82da431e3..103bed164 100644
--- a/pyworkflow/gui/dialog.py
+++ b/pyworkflow/gui/dialog.py
@@ -574,9 +574,11 @@ def askColor(parent, defaultColor='black'):
     return hexcolor
 
 
-def askPath(title="Browse files", msg="Select a file of a folder", path=".", onlyFolders=False, master=None, returnBaseName=False):
+def askPath(title=None, msg="Select a file of a folder", path=".", onlyFolders=False, master=None, returnBaseName=False):
     from pyworkflow.gui.browser import FileBrowserWindow
 
+    if title is None:
+        title = "Select a folder" if onlyFolders else "Select a file"
     browserW = FileBrowserWindow(title, master=master, path=path, onlyFolders=onlyFolders)
     browserW.show(modal=True)
 
@@ -663,36 +665,6 @@ def _createPreviewPanel(self, parent):
     def _createFilterBox(self, content):
         """ Create the Frame with Filter widgets """
 
-        def _onSearch(e=None):
-
-            def comparison():
-                pattern = self._searchVar.get().lower()
-                return [w[0] for w in self.lista.items()
-                        if pattern in self.lista.get(w[0]).lower()]
-
-            self.tree.update()
-            self.lista = {}
-
-            for item in self.tree.get_children():
-
-                itemStr = self.tree.item(item)['text']
-                for value in self.tree.item(item)['values']:
-                    if isinstance(value, int):
-                        itemStr = itemStr + ' ' + str(value)
-                    else:
-                        itemStr = itemStr + ' ' + value
-
-                self.lista[item] = itemStr
-
-            if self._searchVar.get() != '':
-                matchs = comparison()
-                if matchs:
-                    for item in self.tree.get_children():
-                        if item not in matchs:
-                            self.tree.delete(item)
-                else:
-                    self.tree.delete(*self.tree.get_children())
-
         self.searchBoxframe = tk.Frame(content)
         label = tk.Label(self.searchBoxframe, text="Filter")
         label.grid(row=0, column=0, sticky='nw')
@@ -701,12 +673,46 @@ def comparison():
                               textvariable=self._searchVar, width=40,
                               font=gui.getDefaultFont())
 
-        self.entry.bind('<KeyRelease>', _onSearch)
+        self.entry.bind('<KeyRelease>', self._onSearch)
         self.entry.focus_set()
         self.entry.grid(row=0, column=1, sticky='news')
         self.searchBoxframe.grid(row=0, column=0, sticky='news', padx=5,
                                  pady=(10, 5))
 
+    def refresh(self):
+        """ Refreshes the list taking into account the filter"""
+        self._onSearch()
+
+    def _onSearch(self, e=None):
+
+        def comparison():
+            pattern = self._searchVar.get().lower()
+            return [w[0] for w in self.lista.items()
+                    if pattern in self.lista.get(w[0]).lower()]
+
+        self.tree.update()
+        self.lista = {}
+
+        for item in self.tree.get_children():
+
+            itemStr = self.tree.item(item)['text']
+            for value in self.tree.item(item)['values']:
+                if isinstance(value, int):
+                    itemStr = itemStr + ' ' + str(value)
+                else:
+                    itemStr = itemStr + ' ' + value
+
+            self.lista[item] = itemStr
+
+        if self._searchVar.get() != '':
+            matchs = comparison()
+            if matchs:
+                for item in self.tree.get_children():
+                    if item not in matchs:
+                        self.tree.delete(item)
+            else:
+                self.tree.delete(*self.tree.get_children())
+
     def apply(self):
         self.values = self.tree.getSelectedObjects()
 
@@ -760,6 +766,7 @@ def __init__(self, parent, title, provider,
         """
         self.toolbarButtons = toolbarButtons
         self._itemDoubleClick = kwargs.get('itemDoubleClick', None)
+        self._itemOnClick = kwargs.get('itemOnClick', None)
         ListDialog.__init__(self, parent, title, provider, message, **kwargs)
 
     def body(self, bodyFrame):
@@ -781,9 +788,12 @@ def body(self, bodyFrame):
         if self._itemDoubleClick:
             self.tree.itemDoubleClick = self._itemDoubleClick
 
+        if self._itemOnClick:
+            self.tree.itemOnClick = self._itemOnClick
+
     def addButton(self, button, col):
 
-        self._addButton(self.toolbarFrame, button.command, icon=button.icon, col=col, tooltip=button.tooltip, shortcut=button.shortcut)
+        self._addButton(self.toolbarFrame, button.command, text=button.text, icon=button.icon, col=col, tooltip=button.tooltip, shortcut=button.shortcut)
 
 
 class FlashMessage:
diff --git a/pyworkflow/gui/form.py b/pyworkflow/gui/form.py
index 72ff8c582..3176a5d37 100644
--- a/pyworkflow/gui/form.py
+++ b/pyworkflow/gui/form.py
@@ -1537,13 +1537,16 @@ class FormWindow(Window):
         4. Buttons: buttons at bottom for close, save and execute.
     """
 
-    def __init__(self, title, protocol, callback, master=None, **kwargs):
+    def __init__(self, title, protocol, callback, master=None, position=None, **kwargs):
         """ Constructor of the Form window. 
         Params:
          title: title string of the windows.
          protocol: protocol from which the form will be generated.
          callback: callback function to call when Save or Execute are press.
         """
+
+        if position:
+            title = title + " at %s,%s" % position
         Window.__init__(self, title, master, icon=pwutils.Icon.SCIPION_ICON_PROT,
                         weight=False, minsize=(600, 450), **kwargs)
 
@@ -1554,6 +1557,7 @@ def __init__(self, title, protocol, callback, master=None, **kwargs):
         self.disableRunMode = kwargs.get('disableRunMode', False)
         self.bindings = []
         self.protocol = protocol
+        self.position = position
         # This control when to close or not after execute
         self.visualizeMode = kwargs.get('visualizeMode', False)
         self.headerBgColor = pw.Config.SCIPION_MAIN_COLOR
@@ -1918,22 +1922,27 @@ def _editQueueParams(self, e=None):
         # Grab the host config from the project, since it 
         # have not been set in the protocol
         hostConfig = self._getHostConfig()
-        queues = OrderedDict(sorted(hostConfig.queueSystem.queues.items()))
-        # If there is only one Queue and it has no parameters
-        # don't bother to showing the QueueDialog
-        noQueueChoices = len(queues) == 1 and len(list(queues.values())[0]) == 0
-        if noQueueChoices:
-            result = list(queues.keys())[0], {}
+        queues = hostConfig.queueSystem.queues
+        if not queues:
+            self.showError("No queues configured!")
+            return False
         else:
-            dlg = QueueDialog(self, queues)
+            queues = OrderedDict(sorted(queues.items()))
+            # If there is only one Queue and it has no parameters
+            # don't bother to showing the QueueDialog
+            noQueueChoices = len(queues) == 1 and len(list(queues.values())[0]) == 0
+            if noQueueChoices:
+                result = list(queues.keys())[0], {}
+            else:
+                dlg = QueueDialog(self, queues)
 
-            if not dlg.resultYes():
-                return False
-            result = dlg.value
+                if not dlg.resultYes():
+                    return False
+                result = dlg.value
 
-        self.protocol.setQueueParams(result)
-        self.protocol.queueShown = True
-        return True
+            self.protocol.setQueueParams(result)
+            self.protocol.queueShown = True
+            return True
 
     def _createParams(self, parent):
         paramsFrame = tk.Frame(parent, name="params")
@@ -2159,7 +2168,7 @@ def execute(self, e=None):
         elif resultAction == RESULT_RUN_ALL:
             if errors:
                 self.showInfo(errors)
-            self._close()
+            self.close()
             return
 
         # This code will happen when protocol is executed alone
@@ -2183,7 +2192,7 @@ def _close(self, onlySave=False, doSchedule=False):
             # to avoid ghost inputs
             self._checkAllChanges(toggleWidgetVisibility=False)
 
-            message = self.callback(self.protocol, onlySave, doSchedule)
+            message = self.callback(self.protocol, onlySave, doSchedule, position=self.position)
             if not self.visualizeMode:
                 if len(message):
                     self.showInfo(message, "Protocol action")
diff --git a/pyworkflow/gui/gui.py b/pyworkflow/gui/gui.py
index 7236f0f59..142321b72 100644
--- a/pyworkflow/gui/gui.py
+++ b/pyworkflow/gui/gui.py
@@ -228,7 +228,7 @@ def getImage(imageName, imgDict=None, tkImage=True, percent=100,
         elif maxheight and h > maxheight:
             newSize = int(w * float(maxheight) / h), maxheight
         if newSize:
-            image.thumbnail(newSize, Image.ANTIALIAS)
+            image.thumbnail(newSize, Image.LANCZOS)
         if tkImage:
             image = ImageTk.PhotoImage(image)
 
diff --git a/pyworkflow/gui/matplotlib_image.py b/pyworkflow/gui/matplotlib_image.py
index c252bbecc..f9106e948 100644
--- a/pyworkflow/gui/matplotlib_image.py
+++ b/pyworkflow/gui/matplotlib_image.py
@@ -122,12 +122,25 @@ def _createAxes(self):
         self.ax = ax
         
     def _update(self, Z, *args):
-        self.figureimg.set_data(Z)
-        self.figureimg.autoscale()
-        self.figureimg.set(extent=[0, Z.shape[1], 0, Z.shape[0]])
-        self.canvas.draw()
-        
-        
+        # Adjust the Z shape according to the current data structure.
+        if Z.ndim == 4 and Z.shape[1] == 1:
+            # Assuming that the second dimension is redundant
+            ZAdjusted = Z[:, 0, :, :]
+        else:
+            ZAdjusted = Z
+
+        # If ZAdjusted is a grayscale image (it has only one layer)
+        if ZAdjusted.ndim == 3 and ZAdjusted.shape[0] == 1:
+            ZAdjusted = ZAdjusted[0, :, :]
+
+        # Checks if ZAdjusted has a form compatible with set_data
+        if ZAdjusted.ndim == 2 or (ZAdjusted.ndim == 3 and (ZAdjusted.shape[2] == 3 or ZAdjusted.shape[2] == 4)):
+            self.figureimg.set_data(ZAdjusted)
+            self.figureimg.autoscale()
+            self.figureimg.set(extent=[0, ZAdjusted.shape[1], 0, ZAdjusted.shape[0]])
+            self.canvas.draw()
+
+
 class PsdPreview(Preview):
     def __init__(self, master, dim, lf, hf, dpi=72, label="PSD", listenersDict=None):
         Preview.__init__(self, master, dim, dpi, label, listenersDict=listenersDict)
diff --git a/pyworkflow/gui/plotter.py b/pyworkflow/gui/plotter.py
index 7c631ff72..b93b44003 100644
--- a/pyworkflow/gui/plotter.py
+++ b/pyworkflow/gui/plotter.py
@@ -97,7 +97,7 @@ def __init__(self, x=1, y=1, mainTitle="",
             if mainTitle:
                 self.figure.suptitle(mainTitle, fontsize=fontsize + 4)
             if windowTitle:
-                self.figure.canvas.set_window_title(windowTitle) 
+                self.figure.canvas.manager.set_window_title(windowTitle)
             self.plot_count = 0
             self.last_subplot = None
             self.plot_yformat = '%1.2e'
diff --git a/pyworkflow/gui/project/constants.py b/pyworkflow/gui/project/constants.py
index 22325cf5d..31728a358 100644
--- a/pyworkflow/gui/project/constants.py
+++ b/pyworkflow/gui/project/constants.py
@@ -46,7 +46,7 @@
 # For protocols with warnings
 WARNING_COLOR = '#848484'
 
-
+ACTION_NEW = Message.LABEL_NEW
 ACTION_EDIT = Message.LABEL_EDIT
 ACTION_RENAME = Message.LABEL_RENAME
 ACTION_SELECT_FROM = Message.LABEL_SELECT_FROM
@@ -77,6 +77,7 @@
 ACTION_SEARCH = 'Search'
 
 ActionIcons = {
+    ACTION_NEW: Icon.ACTION_NEW,
     ACTION_EDIT: Icon.ACTION_EDIT,
     ACTION_SELECT_FROM: Icon.ACTION_SELECT_FROM,
     ACTION_SELECT_TO: Icon.ACTION_SELECT_TO,
@@ -107,6 +108,7 @@
 
 # TODO: Centralize this and the 2 above collections
 ActionShortCuts = {
+    ACTION_NEW: "<Control-f>",
     # F keys
     ACTION_RENAME: "<F2>",
     ACTION_REFRESH: "<F5>",
diff --git a/pyworkflow/gui/project/project.py b/pyworkflow/gui/project/project.py
index b5c1afd4a..622c5df16 100644
--- a/pyworkflow/gui/project/project.py
+++ b/pyworkflow/gui/project/project.py
@@ -31,27 +31,21 @@
 """
 
 import logging
-
-from .variables import VariablesDialog
-
 logger = logging.getLogger(__name__)
+
 import os
 import threading
 import shlex
 import subprocess
 import socketserver
-import tempfile
 
 import pyworkflow as pw
 import pyworkflow.utils as pwutils
 from pyworkflow.gui.project.utils import OS
-from pyworkflow.project import MenuConfig, ProjectSettings
+from pyworkflow.project import MenuConfig
 from pyworkflow.gui import Message, Icon
 from pyworkflow.gui.browser import FileBrowserWindow
-# Usage commented.
-# from pyworkflow.em.viewers import EmPlotter
-# Moved to Scipion-app
-# from pyworkflow.gui.plugin_manager import PluginManager
+
 from pyworkflow.gui.plotter import Plotter
 from pyworkflow.gui.text import _open_cmd, openTextFileEditor
 from pyworkflow.webservices import ProjectWorkflowNotifier, WorkflowRepository
@@ -90,8 +84,6 @@ def __init__(self, path, master=None):
                             shortCut="Ctrl+t", icon=Icon.ACTION_VISUALIZE)
         projMenu.addSubMenu('Select all protocols', 'select all',
                             shortCut="Ctrl+a", icon=Icon.SELECT_ALL)
-        projMenu.addSubMenu('Add a protocol', 'find protocol',
-                            shortCut="Ctrl+f", icon=Icon.FIND)
         projMenu.addSubMenu('Locate a protocol', 'locate protocol',
                             shortCut="Ctrl+l")
         projMenu.addSubMenu('', '')  # add separator
@@ -163,6 +155,7 @@ def _onClosing(self):
      
     def loadProject(self):
         proj = pw.project.Project(pw.Config.getDomain(), self.projPath)
+        proj.configureLogging()
         proj.load()
 
         # Check if we have settings.sqlite, generate if not
@@ -258,9 +251,6 @@ def onToggleColorMode(self):
     def onSelectAllProtocols(self):
         self.getViewWidget()._selectAllProtocols(None)
 
-    def onAddAProtocol(self):
-        self.getViewWidget()._findProtocol(None)
-
     def onLocateAProtocol(self):
         self.getViewWidget()._locateProtocol(None)
 
diff --git a/pyworkflow/gui/project/searchprotocol.py b/pyworkflow/gui/project/searchprotocol.py
index d88fc47ec..562f8d0bd 100644
--- a/pyworkflow/gui/project/searchprotocol.py
+++ b/pyworkflow/gui/project/searchprotocol.py
@@ -69,14 +69,15 @@ class SearchProtocolWindow(SearchBaseWindow):
         'score': ('Score', {'width': 50, 'stretch': tk.FALSE}, 5, int)
     }
 
-    def __init__(self, parentWindow, **kwargs):
+    def __init__(self, parentWindow, position=None):
 
+        posStr = "" if position is None else " at (%s,%s)" % position
+        self.position = position
         super().__init__(parentWindow,
-                         title="Add a protocol",
-                         **kwargs)
+                         title="Add a protocol" + posStr)
 
     def _createResultsTree(self, frame, show, columns):
-        return self.master.getViewWidget()._createProtocolsTree(frame, show=show, columns=columns)
+        return self.master.getViewWidget()._createProtocolsTree(frame, show=show, columns=columns, position=self.position)
 
     def _onSearchClick(self, e=None):
 
diff --git a/pyworkflow/gui/project/searchrun.py b/pyworkflow/gui/project/searchrun.py
index 0744626fc..db7cbe069 100644
--- a/pyworkflow/gui/project/searchrun.py
+++ b/pyworkflow/gui/project/searchrun.py
@@ -38,13 +38,14 @@ class RunsTreeProvider(ProjectRunsTreeProvider):
 
     def __init__(self, project, actionFunc):
         super().__init__(project)
-        self.actionFunc = actionFunc
+        self.actionFunc = actionFunc  # ProtocolsView._runActionClicked
         self._selection = project.getSettings().runSelection
 
     def getActionsFromSelection(self):
         """ Return the list of options available for selection. """
         n = len(self._selection)
         single = n == 1
+        anyselected = n > 0
         if n:
             prot = self.project.getProtocol(self._selection[0], fromRuns=True)
             status = prot.getStatus()
@@ -57,7 +58,7 @@ def getActionsFromSelection(self):
                                pwprot.STATUS_LAUNCHED]
 
         # This list defines the order the icons are shown
-        return [
+        return [(ACTION_NEW, True),
                 (ACTION_EDIT, single and status and expanded),
                 (ACTION_BROWSE, single and status and expanded),
                 (ACTION_RENAME, single and status and expanded),
@@ -68,8 +69,8 @@ def getActionsFromSelection(self):
                 (ACTION_PASTE, status and expanded),
                 (ACTION_DELETE, status != pwprot.STATUS_RUNNING and status and expanded),
 
-                (ACTION_SELECT_FROM, True),
-                (ACTION_SELECT_TO, True),
+                (ACTION_SELECT_FROM, anyselected),
+                (ACTION_SELECT_TO, anyselected),
                 (ACTION_COLLAPSE, single and status and expanded),
                 (ACTION_EXPAND, single and status and not expanded),
 
@@ -79,20 +80,35 @@ def getActionsFromSelection(self):
                 (ACTION_CONTINUE_WORKFLOW, single),
                 (ACTION_RESET_WORKFLOW, single),
 
-                (ACTION_EXPORT, not single),
-                (ACTION_EXPORT_UPLOAD, not single),
+                (ACTION_EXPORT, anyselected),
+                (ACTION_EXPORT_UPLOAD, anyselected),
 
                 (ACTION_STEPS, single and Config.debugOn() and status and expanded),
                 (ACTION_DB, single and Config.debugOn() and status and expanded),
                 ]
 
-    def getObjectActions(self, obj):
+    def getObjectActions(self, obj, withEvent=False):
+        """ Get actions available to perform.
+
+        This method is called in 2 cases:
+        1.- Right-click on the tree (list of runs)
+        2.- Right-click on the canvas. When called from the canvas we need the event to get the click's position.
+
+        :param obj: optional, if passed, actions on the object. otherwise generic actions
+        :param withEvent: pass True if callback has to have the event as a parameter (call from canvas but not from tree)
+
+        """
 
         def addAction(actionLabel):
             if actionLabel:
                 text = actionLabel
                 action = actionLabel
-                actionLabel = (text, lambda: self.actionFunc(action),
+                if withEvent:
+                    callback=lambda e: self.actionFunc(action, e)
+                else:
+                    callback=lambda: self.actionFunc(action)
+
+                actionLabel = (text, callback,
                                ActionIcons.get(action, None),
                                ActionShortCuts.get(action,None))
             return actionLabel
@@ -100,7 +116,7 @@ def addAction(actionLabel):
         actions = [addAction(a)
                    for a, cond in self.getActionsFromSelection() if cond]
 
-        if hasattr(obj, 'getActions'):
+        if obj is not None and hasattr(obj, 'getActions'):
             for text, action in obj.getActions():
                 actions.append((text, action, None))
 
diff --git a/pyworkflow/gui/project/variables.py b/pyworkflow/gui/project/variables.py
index ada3e654f..6922cc094 100644
--- a/pyworkflow/gui/project/variables.py
+++ b/pyworkflow/gui/project/variables.py
@@ -99,7 +99,7 @@ def _editVariable(self, e=None):
             variable = selection[0]
             dlg = EditVariableDialog(self, "Edit variable", variable)
             if dlg.resultYes():
-                self.tree.update()
+                self.refresh()
 
     def _setToDefault(self, e=None):
         selection = self.tree.getSelectedObjects()
@@ -163,7 +163,7 @@ def buttonClicked(self, e):
             value =askPath(path=path, master=self, onlyFolders=onlyFolders, returnBaseName=returnBaseName)
             self.valueVar.set(value)
     def apply(self):
-        self.variable.value = self.valueVar.get()
+        self.variable.setValue(self.valueVar.get())
 
     def validate(self):
 
diff --git a/pyworkflow/gui/project/viewdata.py b/pyworkflow/gui/project/viewdata.py
index 9c42ec67d..3c19fe766 100644
--- a/pyworkflow/gui/project/viewdata.py
+++ b/pyworkflow/gui/project/viewdata.py
@@ -367,8 +367,8 @@ def _loopData(self, action):
 
         return results
 
-    def _onDoubleClick(self, e=None):
-        if e.node.pointer:
+    def _onDoubleClick(self, item=None, e=None):
+        if item.node.pointer:
             self._selectObject(e.node.pointer)
             self._viewObject(e.node.pointer.get().getObjId())
             return
@@ -422,15 +422,14 @@ def _executeSaveProtocol(self, prot, onlySave=False):
 
         return msg
 
-    def _onRightClick(self, e=None):
-        return [
-            (pwutils.Message.LABEL_EDIT, self._editObject,
-             pwutils.Icon.ACTION_EDIT),
-            ('Go to protocol', self._goToProtocol,
-             pwutils.Icon.ACTION_SEARCH)
-        ]
+    def _onRightClick(self, item=None, e=None):
+        return []
+        #
+        #     (pwutils.Message.LABEL_EDIT, self._editObject, pwutils.Icon.ACTION_EDIT),
+        #     ('Go to protocol', self._goToProtocol, pwutils.Icon.ACTION_SEARCH)
+        # ]
     
-    def _editObject(self):
+    def _editObject(self, e=None):
         """Open the Edit GUI Form given an instance"""
         EditObjectDialog(self, pwutils.Message.TITLE_EDIT_OBJECT,
                          self._selected, self.project.mapper)
diff --git a/pyworkflow/gui/project/viewprotocols.py b/pyworkflow/gui/project/viewprotocols.py
index 7c784306c..aac91df56 100644
--- a/pyworkflow/gui/project/viewprotocols.py
+++ b/pyworkflow/gui/project/viewprotocols.py
@@ -106,7 +106,6 @@ def __init__(self, parent, window, **args):
         self._lastRightClickPos = None  # Keep last right-clicked position
 
         self.style = ttk.Style()
-        self.root.bind("<Control-f>", self._findProtocol)
         self.root.bind("<Control-a>", self._selectAllProtocols)
         self.root.bind("<Control-t>", self._toggleColorScheme)
         self.root.bind("<Control-D>", self._toggleDebug)
@@ -264,16 +263,15 @@ def createContent(self):
                                                       font=self.window.font)
         self.projLog.grid(row=0, column=0, sticky='news')
         self.projLog.windows = self.window
-        self.projLog.addFile(Config.SCIPION_LOG) # TODO: Have a project specific log
-
-        self._updateSelection()
+        self.projLog.addFile(self.project.getProjectLog())
 
         # Move to the selected protocol
         if self._isSingleSelection():
             prot = self.getSelectedProtocol()
             node = self.runsGraph.getNode(str(prot.getObjId()))
             self._selectNode(node)
-
+        else:
+            self._updateSelection()
 
         # Add all tabs
 
@@ -331,7 +329,7 @@ def info(self, message):
     def cleanInfo(self):
         self.info("")
 
-    def refreshRuns(self, e=None, initRefreshCounter=True, checkPids=False):
+    def refreshRuns(self, e=None, initRefreshCounter=True, checkPids=False, position=None):
         """
         Refresh the protocol runs workflow. If the variable REFRESH_WITH_THREADS
         exits, then use a threads to refresh, i.o.c use normal behavior
@@ -350,10 +348,10 @@ def refreshRuns(self, e=None, initRefreshCounter=True, checkPids=False):
             else:
                 self.repeatRefresh = True
         else:
-            self.refreshDisplayedRuns(e, initRefreshCounter, checkPids)
+            self.refreshDisplayedRuns(e, initRefreshCounter, checkPids, position=position)
 
     # noinspection PyUnusedLocal
-    def refreshDisplayedRuns(self, e=None, initRefreshCounter=True, checkPids=False):
+    def refreshDisplayedRuns(self, e=None, initRefreshCounter=True, checkPids=False, position=None):
         """ Refresh the status of displayed runs.
          Params:
             e: Tk event input
@@ -364,21 +362,11 @@ def refreshDisplayedRuns(self, e=None, initRefreshCounter=True, checkPids=False)
         self.viewButtons[ACTION_REFRESH]['state'] = tk.DISABLED
         self.info('Refreshing...')
         self.refreshSemaphore = False
-        if Config.debugOn():
-            import psutil
-            proc = psutil.Process(os.getpid())
-            mem = psutil.virtual_memory()
-            logger.debug("------------- refreshing ---------- ")
-            files = proc.open_files()
-            logger.debug("  open files: %s" % len(files))
-            for f in files:
-                logger.debug("    - %s, %s" % (f.path, f.fd))
-            logger.debug("  memory percent: %s" % proc.memory_percent())
 
         if self.runsView == VIEW_LIST:
             self.updateRunsTree(True)
         else:
-            self.updateRunsGraph(True, checkPids=checkPids)
+            self.updateRunsGraph(True, checkPids=checkPids, position=position)
             self._updateSelection()
 
         if initRefreshCounter:
@@ -410,9 +398,15 @@ def _automaticRefreshRuns(self, e=None):
                                                  self._automaticRefreshRuns)
 
     # noinspection PyUnusedLocal
-    def _findProtocol(self, e=None):
+    def _findProtocol(self, event=None):
         """ Find a desired protocol by typing some keyword. """
-        window = SearchProtocolWindow(self.window)
+
+        if event is not None and event.widget.widgetName=="canvas":
+            position = self.runsGraphCanvas.getCoordinates(event)
+        else:
+            position = None
+
+        window = SearchProtocolWindow(self.window, position=position)
         window.show()
 
     def _locateProtocol(self, e=None):
@@ -449,7 +443,7 @@ def createActionToolbar(self):
 
         self.actionButtons = {}
         actionList = [
-            ACTION_EDIT, ACTION_RENAME, ACTION_DUPLICATE, ACTION_COPY, ACTION_PASTE,  ACTION_DELETE,
+            ACTION_NEW, ACTION_EDIT, ACTION_RENAME, ACTION_DUPLICATE, ACTION_COPY, ACTION_PASTE,  ACTION_DELETE,
             ACTION_BROWSE,
             ACTION_STOP, ACTION_STOP_WORKFLOW, ACTION_CONTINUE, ACTION_CONTINUE_WORKFLOW, ACTION_RESTART_WORKFLOW, ACTION_RESET_WORKFLOW,
             ACTION_RESULTS,
@@ -476,7 +470,7 @@ def addButton(action, text, toolbar):
                 text += " (%s)" % shortCut
                 self.root.bind(shortCut, callback)
 
-            ToolTip(btn,text , 500)
+            ToolTip(btn, text, 500)
 
             return btn
 
@@ -540,7 +534,7 @@ def displayAction(actionToDisplay, column, condition=True):
             displayAction(action, i, cond)
 
     def _createProtocolsTree(self, parent,
-                             show='tree', columns=None):
+                             show='tree', columns=None, position=None):
 
         t = pwgui.tree.Tree(parent, show=show, style=LIST_TREEVIEW,
                             columns=columns)
@@ -549,9 +543,9 @@ def _createProtocolsTree(self, parent,
         def configureTag(tag, img):
             # Protocol nodes
             t.tag_configure(tag, image=self.getImage(img))
-            t.tag_bind(tag, TK.LEFT_DOUBLE_CLICK, self._protocolItemClick)
-            t.tag_bind(tag, TK.RETURN, self._protocolItemClick)
-            t.tag_bind(tag, TK.ENTER, self._protocolItemClick)
+            t.tag_bind(tag, TK.LEFT_DOUBLE_CLICK, lambda e: self._protocolItemClick(e, position))
+            t.tag_bind(tag, TK.RETURN, lambda e: self._protocolItemClick(e, position))
+            t.tag_bind(tag, TK.ENTER, lambda e: self._protocolItemClick(e, position))
 
         # Protocol nodes
         configureTag(ProtocolTreeConfig.TAG_PROTOCOL, Icon.PRODUCTION)
@@ -751,13 +745,13 @@ def createRunsGraph(self, parent):
 
         self.updateRunsGraph()
 
-    def updateRunsGraph(self, refresh=False, checkPids=False):
+    def updateRunsGraph(self, refresh=False, checkPids=False, position=None):
 
         self.runsGraph = self.project.getRunsGraph(refresh=refresh,
                                                    checkPids=checkPids)
-        self.drawRunsGraph()
+        self.drawRunsGraph(position=position)
 
-    def drawRunsGraph(self, reorganize=False):
+    def drawRunsGraph(self, reorganize=False, position=None):
 
         # Check if there are positions stored
         if reorganize:
@@ -773,7 +767,9 @@ def drawRunsGraph(self, reorganize=False):
                 nodeId = node.run.getObjId() if node.run else 0
                 nodeInfo = self.settings.getNodeById(nodeId)
                 if nodeInfo is None:
-                    self.settings.addNode(nodeId, x=0, y=0, expanded=True,
+                    if position is None:
+                        position = (0,0)
+                    self.settings.addNode(nodeId, x=position[0], y=position[1], expanded=True,
                                           visible=True)
 
             self.runsGraphCanvas.drawGraph(self.runsGraph, layout,
@@ -1038,7 +1034,9 @@ def switchRunsView(self):
             self.runsGraphCanvas.frame.grid(row=0, column=0, sticky='news')
             self.viewButtons[ACTION_TREE].grid(row=0, column=1)
 
-    def _protocolItemClick(self, e=None):
+    def _protocolItemClick(self, e=None, position=None):
+        """ Callback for the window to add a new protocol."""
+
         # Get the tree widget that originated the event
         # it could be the left panel protocols tree or just
         # the search protocol dialog tree
@@ -1046,7 +1044,7 @@ def _protocolItemClick(self, e=None):
         protClassName = tree.getFirst().split('.')[-1]
         protClass = self.domain.getProtocols().get(protClassName)
         prot = self.project.newProtocol(protClass)
-        self._openProtocolForm(prot, disableRunMode=True)
+        self._openProtocolForm(prot, disableRunMode=True, position=position)
 
     def _toggleColorScheme(self, e=None):
 
@@ -1163,8 +1161,8 @@ def _selectItemProtocol(self, prot):
         self._updateSelection()
         self.runsGraphCanvas.update_idletasks()
 
-    def _deselectItems(self, item):
-        """ Deselect all items except the item one
+    def _deselectItems(self, exception):
+        """ Deselect all items except the item one. Pass item=None to deselect all
         """
         g = self.project.getRunsGraph()
 
@@ -1173,9 +1171,14 @@ def _deselectItems(self, item):
                 # This option is only for compatibility with all projects
                 if hasattr(node, 'item'):
                     node.item.setSelected(False)
-        item.setSelected(True)
 
-    def _runItemClick(self, item=None):
+        # clear the selection
+        self._selection.clear()
+
+        if exception:
+            exception.setSelected(True)
+            self._selection.append(exception.id)
+    def _runItemClick(self, item=None, event=None):
 
         # If click is in a empty area....start panning
         if item is None:
@@ -1193,28 +1196,41 @@ def _runItemClick(self, item=None):
             self._deselectItems(item)
         self._selectItemProtocol(prot)
 
-    def _runItemDoubleClick(self, e=None):
-        if e.nodeInfo.isExpanded():
+    def _runItemDoubleClick(self, item=None, e=None):
+        if item.nodeInfo.isExpanded():
             self._runActionClicked(ACTION_EDIT)
 
     def _runItemMiddleClick(self, e=None):
         self._runActionClicked(ACTION_SELECT_TO)
 
-    def _runItemRightClick(self, item=None):
-        prot = item.node.run
-        if prot is None:  # in case it is the main "Project" node
-            return
-        n = len(self._selection)
-        # Only select item with right-click if there is a single
-        # item selection, not for multiple selection
-        if n <= 1:
-            self._deselectItems(item)
-            self._selectItemProtocol(prot)
-            self._lastRightClickPos = self.runsGraphCanvas.eventPos
+    def _runItemRightClick(self, item=None, e=None):
+        """ Right click on the canvas callback
+
+        :param item: item right-clicked. None if clicked in the void
+        :param e: event object with context information"""
+        prot = None
+        # If there's been a click in a box
+        if item is not None:
+
+            # Get the protocol associated
+            prot = item.node.run
 
-        return self.provider.getObjectActions(prot)
+            if prot is None:  # in case it is the main "Project" node
+                return
 
-    def _runItemControlClick(self, item=None):
+            # Only select item with right-click if there is a single
+            # item selection, not for multiple selection
+            if len(self._selection) == 1:
+                self._deselectItems(item)
+                self._selectItemProtocol(prot)
+                self._lastRightClickPos = self.runsGraphCanvas.eventPos
+        else:  # Click on empty area
+            self._deselectItems(None)
+            self._updateSelection()
+
+        return self.provider.getObjectActions(prot,withEvent=True)
+
+    def _runItemControlClick(self, item=None, event=None):
         # Get last selected item for tree or graph
         if self.runsView == VIEW_LIST:
             # TODO: Prot is not used!!
@@ -1316,13 +1332,13 @@ def _selectItemsWithinArea(x1, y1, x2, y2, enclosed=False):
         #
         # if update is not None: self._updateSelection()
 
-    def _openProtocolForm(self, prot, disableRunMode=False):
+    def _openProtocolForm(self, prot, disableRunMode=False, position=None):
         """Open the Protocol GUI Form given a Protocol instance"""
 
         w = FormWindow(Message.TITLE_NAME_RUN + prot.getClassName(),
                        prot, self._executeSaveProtocol, self.window,
                        updateProtocolCallback=self._updateProtocol,
-                       disableRunMode=disableRunMode)
+                       disableRunMode=disableRunMode, position=position)
         w.adjustSize()
         w.show(center=True)
 
@@ -1477,9 +1493,9 @@ def _fillLogs(self):
             import traceback
             traceback.print_exc()
 
-    def _scheduleRunsUpdate(self, secs=1):
+    def _scheduleRunsUpdate(self, secs=1, position=None):
         # self.runsTree.after(secs*1000, self.refreshRuns)
-        self.window.enqueue(self.refreshRuns)
+        self.window.enqueue(lambda : self.refreshRuns(position=position))
 
     def executeProtocol(self, prot):
         """ Function to execute a protocol called not
@@ -1489,7 +1505,7 @@ def executeProtocol(self, prot):
         # to be executed in the same thread
         self.window.enqueue(lambda: self._executeSaveProtocol(prot))
 
-    def _executeSaveProtocol(self, prot, onlySave=False, doSchedule=False):
+    def _executeSaveProtocol(self, prot, onlySave=False, doSchedule=False, position=None):
         if onlySave:
             self.project.saveProtocol(prot)
             msg = Message.LABEL_SAVED_FORM
@@ -1509,7 +1525,7 @@ def _executeSaveProtocol(self, prot, onlySave=False, doSchedule=False):
 
         # Update runs list display, even in save we
         # need to get the updated copy of the protocol
-        self._scheduleRunsUpdate()
+        self._scheduleRunsUpdate(position=position)
         self._selectItemProtocol(prot)
 
         return msg
@@ -1742,17 +1758,18 @@ def _launchSubWorkflow(protocol, mode, root, askSingleAll=False):
                 return [], RESULT_RUN_SINGLE
 
     def _selectLabels(self):
+
+        dlg = self.window.manageLabels()
+
         selectedNodes = self._getSelectedNodes()
 
-        if selectedNodes:
-            dlg = self.window.manageLabels()
+        if dlg.resultYes() and selectedNodes:
 
-            if dlg.resultYes():
-                for node in selectedNodes:
-                    node.setLabels([label.getName() for label in dlg.values])
+            for node in selectedNodes:
+                node.setLabels([label.getName() for label in dlg.values])
 
-                # self.updateRunsGraph()
-                self.drawRunsGraph()
+            # self.updateRunsGraph()
+            self.drawRunsGraph()
 
             # Save settings in any case
             self.window.saveSettings()
@@ -1917,7 +1934,7 @@ def _bibExportClicked(self, e=None):
                             jsonStr = json.dumps(refDict, indent=4,
                                                  ensure_ascii=False)[1:]
                             jsonStr = jsonStr.replace('": "', '"= "')
-                            jsonStr = re.sub('(?<!= )"(\S*?)"', '\\1', jsonStr)
+                            jsonStr = re.sub(r'(?<!= )"(\S*?)"', '\\1', jsonStr)
                             jsonStr = jsonStr.replace('= "', ' = "')
                             refStr = '@%s{%s,%s\n\n' % (refType, refId, jsonStr)
                             bibFile.write(refStr.encode('utf-8'))
@@ -1961,6 +1978,10 @@ def _runActionClicked(self, action, event=None):
 
         elif action == ACTION_SWITCH_VIEW:
             self.switchRunsView()
+        elif action == ACTION_NEW:
+            self._findProtocol(event)
+        elif action == ACTION_LABELS:
+            self._selectLabels()
         else:
             prot = self.getSelectedProtocol()
             if prot:
@@ -2007,8 +2028,7 @@ def _runActionClicked(self, action, event=None):
                         self.setVisibleNodes(node, visible=True)
                         self.updateRunsGraph(False)
                         self._updateActionToolbar()
-                    elif action == ACTION_LABELS:
-                        self._selectLabels()
+
                     elif action == ACTION_SELECT_FROM:
                         self._selectDescendants()
                     elif action == ACTION_SELECT_TO:
diff --git a/pyworkflow/gui/text.py b/pyworkflow/gui/text.py
index 76053943b..916865731 100644
--- a/pyworkflow/gui/text.py
+++ b/pyworkflow/gui/text.py
@@ -62,7 +62,9 @@ def find_prog(*args):
         return None
 
     x_open = find_prog('xdg-open', 'gnome-open', 'kde-open', 'gvfs-open')
-    editor = find_prog('pluma', 'gedit', 'kwrite', 'geany', 'kate',
+    editor = pw.Config.SCIPION_TEXT_EDITOR
+    if not editor:
+        editor = find_prog('pluma', 'gedit', 'kwrite', 'geany', 'kate',
                        'emacs', 'nedit', 'mousepad', 'code')
 
     def _open_cmd(path, tkParent=None):
@@ -256,7 +258,7 @@ def openPath(self, path):
         else:
             path = os.path.join(dirname, fname)
 
-        if os.path.exists(path):
+        if os.path.exists(path) or path.startswith("http"):
             from pwem import emlib
             fn = emlib.FileName(path)
             if fn is not None and (fn.isImage() or fn.isMetaData()):
diff --git a/pyworkflow/gui/tree.py b/pyworkflow/gui/tree.py
index fe85c478b..1d1dc13ed 100644
--- a/pyworkflow/gui/tree.py
+++ b/pyworkflow/gui/tree.py
@@ -304,6 +304,8 @@ def setProvider(self, provider):
 
     def _onClick(self, e=None):
         self._unpostMenu()
+        if hasattr(self, 'itemOnClick'):
+            self.itemOnClick(e)
 
     def _onKeyPress(self, e=None):
         self._unpostMenu()
@@ -648,7 +650,7 @@ def getObjectInfo(self, obj):
         return info
 
     def getText(self, obj):
-        return obj.pluginName + '-' + obj.templateName
+        return obj.source + '-' + obj.name
 
     def getValues(self, obj):
         return (obj.description,)
diff --git a/pyworkflow/mapper/__init__.py b/pyworkflow/mapper/__init__.py
index 14ed074b9..a77eecf4e 100644
--- a/pyworkflow/mapper/__init__.py
+++ b/pyworkflow/mapper/__init__.py
@@ -24,4 +24,3 @@
 # **************************************************************************
 from .mapper import Mapper
 from .sqlite import SqliteMapper, SqliteDb, SqliteFlatMapper, SqliteFlatDb
-from .xmlmapper import XmlMapper
diff --git a/pyworkflow/mapper/mapper.py b/pyworkflow/mapper/mapper.py
index fdf673396..dfa130208 100644
--- a/pyworkflow/mapper/mapper.py
+++ b/pyworkflow/mapper/mapper.py
@@ -64,10 +64,12 @@ def warning(self, msg):
             print("WARNING: %s" % msg)
             self.__warnings.add(msg)
     
-    def _buildObjectFromClass(self, className, **kwargs):
+    def _buildObjectFromClass(self, className):
         """ Build an instance of an object
         given the class name, it should be in 
-        the classes dictionary.
+        the classes' dictionary.
+
+        If class is missing, then the default class is used ("LegacyProtocol") and warns about it
         """
         if className not in self.dictClasses:
             self.warning("Class '%s' not found in mapper dict. Ignored. "
@@ -81,11 +83,11 @@ def _buildObjectFromClass(self, className, **kwargs):
             return None
 
         try:
-            instance = self.dictClasses[className](**kwargs)
+            instance = self.dictClasses[className]()
         except Exception as e:
             clazz = self.dictClasses._default
             logger.error('Class %s could not be created. Replacing it with %s ' % (className, clazz.__name__), exc_info=e)
-            instance = clazz(**kwargs)
+            instance = clazz()
 
         # If it's the default class in the dictionary
         if objClass.__name__ != className:
diff --git a/pyworkflow/mapper/sqlite.py b/pyworkflow/mapper/sqlite.py
index d214649a3..b5b9697dd 100644
--- a/pyworkflow/mapper/sqlite.py
+++ b/pyworkflow/mapper/sqlite.py
@@ -27,6 +27,7 @@
 import re
 from collections import OrderedDict
 
+from pyworkflow import Config
 from pyworkflow.utils import replaceExt, joinExt, valueToList
 from .sqlite_db import SqliteDb, OperationalError
 from .mapper import Mapper
@@ -903,38 +904,70 @@ def selectById(self, objId):
         return obj
 
     def __loadObjDict(self):
-        """ Load object properties and classes from db. """
+        """ Load object properties and classes from db.
+        Stores the _objTemplate for future reuse"""
         # Create a template object for retrieving stored ones
         columnList = []
         rows = self.db.getClassRows()
 
         # Adds common fields to the mapping
+        # Schema definition in classes table
         self.db.addCommonFieldsToMap()
 
         attrClasses = {}
         self._objBuildList = []
 
+        # For each row lin classes table (_samplinRate --> c01, Integer)
         for r in rows:
+
+            # Something like: _acquisition._doseInitial
             label = r['label_property']
 
+            # If the actual item class:  "Particle" in a SetOfParticles
+            # First loop.
             if label == SELF:
-                self._objClassName = r['class_name']
-                self._objTemplate = self._buildObjectFromClass(self._objClassName)
+                objClassName = r['class_name']
+
+                # Store the template to reuse it during iterations and avoid instantiation
+                self._objTemplate = self._buildObjectFromClass(objClassName)
+                self._objClass = self._objTemplate.__class__
             else:
-                # Lets update the database column mapping
+                # Update the database column mapping: c01 <-> _samplingRate
                 self.db._columnsMapping[label] = r['column_name']
+
+                # List for the latter _objColumns [(5,"_smplingRate"), ...]
+                # This latter will be used to take the value from the cursor's row using the index (row[5] => obj._samplingRate)
                 columnList.append(label)
+
+                # Annotate the class
                 attrClasses[label] = r['class_name']
+
+                # Split the label: _acquisition._doseInitial -> ["_acquisition", "_doseInitial"]
+                # For the loop
                 attrParts = label.split('.')
+
+                # Like a breadcrumb in websites... partial path to the attribute
                 attrJoin = ''
+
+                # Start from the template (root)
                 o = self._objTemplate
+
+                # for each part: ["_acquisition", "_doseInitial"]
                 for a in attrParts:
                     attrJoin += a
+
+                    # Try to get the attribute. Case of attributes defined in the model (init)
                     attr = getattr(o, a, None)
+
+                    # If the object does not have the attribute, then it might be an extra parameter or an optional like Transform.p+ç1
                     if attr is None:
                         className = attrClasses[attrJoin]
-                        self._objBuildList.append((className, attrJoin.split('.')))
+
+                        # Instantiate the class.
                         attr = self._buildObjectFromClass(className)
+
+                        # Get the class form the attr: it could come as a LegacyClass in case "className" is not found.
+                        self._objBuildList.append((attr.__class__, attrJoin.split('.')))
                         setattr(o, a, attr)
                     o = attr
                     attrJoin += '.'
@@ -943,23 +976,39 @@ def __loadObjDict(self):
         self._objColumns = list(zip(range(basicRows, n), columnList))
          
     def __buildAndFillObj(self):
-        obj = self._buildObjectFromClass(self._objClassName)
+        """ Instantiates the set item base on the _objBuildList.
+        _objBuildList has been populated when loading the classDictionary"""
+
+        obj = self._objClass()
         
-        for className, attrParts in self._objBuildList:
+        for clazz, attrParts in self._objBuildList:
             o = obj
             for a in attrParts:
                 attr = getattr(o, a, None)
                 if not attr:
-                    setattr(o, a, self._buildObjectFromClass(className))
+                    setattr(o, a, clazz())
                     break
                 o = attr
         return obj
-        
-    def __objFromRow(self, objRow):
+
+    def getInstance(self):
+
         if self._objTemplate is None:
             self.__loadObjDict()
+
+        # Difference in performance using scipion3 tests pwperformance.tests.test_set_performance.TestSetPerformanceSteps.testSuperExtendedCoordinatesSet
+        # A test that creates a 10**6 set of extended coordinates (coordinates with 20 extra attributes)
+        # With the template iteration takes 10 secs
+        # Building the object each time take 25 secs (15 seconds more)
+        if Config.SCIPION_MAPPER_USE_TEMPLATE:
+            return self._objTemplate
+        else:
+            return  self.__buildAndFillObj()
+
+    def __objFromRow(self, objRow):
+
             
-        obj = self._objTemplate  # self.__buildAndFillObj()
+        obj = self.getInstance()
         obj.setObjId(objRow[ID])
         obj.setObjLabel(self._getStrValue(objRow['label']))
         obj.setObjComment(self._getStrValue(objRow['comment']))
@@ -1009,9 +1058,10 @@ def selectAll(self, iterate=True, objectFilter=None, orderBy=ID,
         # 'Properties' table
         if not self.db.hasTable('Properties'):
             return iter([]) if iterate else []
-            
-        if self._objTemplate is None:
-            self.__loadObjDict()
+
+        # Initialize the instance
+        self.getInstance()
+
         try:
             objRows = self.db.selectAll(orderBy=orderBy,
                                         direction=direction,
diff --git a/pyworkflow/mapper/xmlmapper.py b/pyworkflow/mapper/xmlmapper.py
deleted file mode 100644
index 0981713b1..000000000
--- a/pyworkflow/mapper/xmlmapper.py
+++ /dev/null
@@ -1,264 +0,0 @@
-# **************************************************************************
-# *
-# * Authors:     J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
-# *              Roberto Marabini       (roberto@cnb.csic.es)
-# *
-# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
-# *
-# * 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 3 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
-# *
-# *  All comments concerning this program package may be sent to the
-# *  e-mail address 'scipion@cnb.csic.es'
-# *
-# **************************************************************************
-
-from os.path import exists
-
-try:
-    import xml.etree.cElementTree as ET
-except ImportError:
-    import xml.etree.ElementTree as ET
-
-from .mapper import Mapper
-
-
-class XmlMapper(Mapper):
-    """Mapper for XML"""
-    def __init__(self, filename, dictClasses=None, rootName='ALL', **args):
-        self.filename = filename        
-        if exists(filename):
-            self._read()           
-        else:
-            self._create(rootName, **args)
-        Mapper.__init__(self, dictClasses)
-        # Objects map (id should be defined)  
-        self.objDict = {}      
-        # Store some objects during parsing
-        # that have some Pointer attributes and need to be fixed later 
-        self.pendingPtrDict = {}
-        # This dictionary serve to define how to write classes
-        # for example if the pair 'Integer': 'attribute' is present
-        # all Integer will be store as attributes in the xml
-        # possible values are: 
-        #   'attribute', 'class_only', 'class_name', 'name_class', 'name_only'
-        self.classTags = {} 
-        # Counter to provide default objects id's
-        self.objCount = 0
-
-    def setClassTag(self, classname, tag):
-        self.classTags[classname] = tag
-        
-    def indent(self, elem, level=0):
-        i = "\n" + level*"  "
-        if len(elem):
-            if not elem.text or not elem.text.strip():
-                elem.text = i + "  "
-            if not elem.tail or not elem.tail.strip():
-                elem.tail = i
-            for _elem in elem:
-                self.indent(_elem, level+1)
-            if not _elem.tail or not _elem.tail.strip():
-                _elem.tail = i
-        else:
-            # TODO: generalize the matching of consecutive items
-            if level and (not elem.tail or not elem.tail.strip()):
-                for ii in ['t11', 't12', 't13', 't21', 't22', 't23', 't31', 't32', 't33']:
-                    if elem.tag == ii:
-                        elem.tail = " "
-                        return
-                elem.tail = i
-    
-    def _create(self, rootName, **args):
-        self.root = ET.Element(rootName)
-        if 'header' in args:
-            self.root.append(ET.Comment(args.get('header')))
-            if 'version' in args:
-                self.root.set("version", str(args.get('version')))
-    
-    def _read(self):
-        if self.filename is None:
-            raise Exception("XmlMapper.read: filename is None.")
-        if not exists(self.filename):
-            raise Exception("XmlMapper.read: filename '%s' does not exists.")
-        self.tree = ET.parse(self.filename)
-        self.root = self.tree.getroot()
-
-    def strId(self, objId):
-        """Create an unique id from the objId,
-        specially for dictionaries, where all keys
-        and values will be converted to string"""
-        if type(objId) is dict:
-            ordKeys = objId.keys()
-            ordKeys.sort()
-            sid = ''
-            for k in ordKeys:
-                sid += '%s:%s ' % (k, objId[k])
-            return sid
-        return str(objId)
-        
-    def selectById(self, objId):
-        """Retrieve an object give the id"""
-        return self.objDict.get(self.strId(objId), None)
-    
-    def _addObjectToDict(self, obj, objDict):
-        """Add object if have id"""
-        if obj.hasObjId():
-            key = self.strId(obj.getObjId())
-        else:
-            self.objCount += 1
-            key = '%s_%06d' % (obj.getClassName(), self.objCount)
-        objDict[key] = obj
-        
-    def selectAll(self, iterate=False):
-        """Select object from storage"""
-        for child in self.root:
-            obj = self._buildObjectFromClass(child.tag)
-            self.fillObject(obj, child)
-            self._addObjectToDict(obj, self.objDict)
-        # set properly primary keys
-        for obj in self.pendingPtrDict.values():
-            for key, attr in obj.getAttributesToStore():
-                if attr.isPointer(): 
-                    ptr = self.selectById(attr._objId)
-                    attr._objId = None
-                    attr.set(ptr)
-        return self.objDict.values()
-                   
-    def setChildObject(self, obj, childName, childClass=None):
-        childObj = getattr(obj, childName, None)
-        if childObj is None:
-            if childClass is None:
-                raise Exception('Attribute "%s" was not found in parent object and classname not stored' % childName)
-            childObj = self._buildObjectFromClass(childClass)
-            setattr(obj, childName, childObj)
-        return childObj
-                  
-    def fillObject(self, obj, objElem):
-        # Set attributes first
-        if not obj.isPointer():
-            for k, v in objElem.attrib.items():
-                if k not in ['id', 'classname', 'attrname']:
-                    childObj = self.setChildObject(obj, k)
-                    childObj.set(v)
-        # Set tree childs attributes
-        for child in objElem:
-            if 'classname' in child.attrib:
-                childName = child.tag
-                childClass = child.attrib['classname']
-            elif 'attrname' in child.attrib:
-                childName = child.attrib['attrname']
-                childClass = child.tag
-            else:
-                tagKey = '%s.%s' % (obj.getClassName(), child.tag)
-                
-                tag = self.classTags.get(tagKey, None)
-                if tag == 'name_only' or child.tag not in self.dictClasses:
-                    childName = child.tag
-                    childClass = None
-                else:
-                    childName = ''
-                    childClass = child.tag
-                
-            childObj = self.setChildObject(obj, childName, childClass)
-            
-            if child.text and len(child.text.strip()):
-                childObj.set(child.text.strip())
-            else:
-                # does have attributes?
-                attributes = child.attrib
-                if attributes and childObj.isPointer():
-                    childObj._objId = attributes
-                    # save obj in auxiliary file
-                    self._addObjectToDict(obj, self.pendingPtrDict)
-                self.fillObject(childObj, child)
-        
-    def commit(self):
-        if self.filename is None:
-            raise Exception("XmlMapper.commit: filename is None")
-        self.indent(self.root)
-        tree = ET.ElementTree(self.root)
-        tree.write(self.filename,
-                   xml_declaration=True,
-                   encoding='utf-8')
-
-    def setObjectId(self, objElem, objId):
-        # Set attributes of this object element
-        if objId:
-            if type(objId) is dict:
-                for k, v in objId.items():
-                    if v:
-                        objElem.set(k, str(v))
-            else:        
-                objElem.set('id', str(objId))
-        
-    def insert(self, obj):
-        """Insert a new object into the system"""
-        objElem = self.addSubElement(self.root, obj.getClassName(), obj._objValue) 
-        self.setObjectId(objElem, obj.getObjId())
-        # Insert object childs
-        self.insertObjectWithChilds(obj, objElem)
-        
-    def addSubElement(self, parentElem, name, value=None):
-        childElem = ET.SubElement(parentElem, name)
-        if value is not None:
-            childElem.text = str(value)
-        return childElem
-            
-    def insertObjectWithChilds(self, obj, parentElem):
-        
-        for key, attr in obj.getAttributesToStore():            
-            if attr.hasValue():
-                attrClass = attr.getClassName()
-                objClass = obj.getClassName()
-                allKey = '%s.ALL' % objClass  # First try with .ALL
-                tag = self.classTags.get(allKey, '')
-                allKey = 'ALL.%s' % attrClass 
-                tag = self.classTags.get(allKey, tag)
-                allKey = 'ALL.%s' % key  # also with ALL.attribute
-                tag = self.classTags.get(allKey, tag)
-                classKey = '%s.%s' % (objClass, attrClass)  # Second, with .childClass
-                tag = self.classTags.get(classKey, tag)
-                attrKey = '%s.%s' % (objClass, key)  # Finally with .attribute
-                tag = self.classTags.get(attrKey, tag)
-                
-                if tag == 'attribute':
-                    parentElem.set(key, str(attr))
-                else:
-                    if attr.isPointer():
-                        childElem = self.addSubElement(parentElem, key)
-                        self.setObjectId(childElem, attr.get().getObjId())
-                    else:
-                        # Select if we want to use the classname or elem name
-                        # as element tagname
-                        if tag.startswith('class'):
-                            name = attrClass
-                        else:
-                            name = key
-                        childElem = self.addSubElement(parentElem, name, attr._objValue)
-                        if tag.endswith('class'):
-                            childElem.set('classname', attrClass)
-                        elif tag.endswith('name'):
-                            childElem.set('attrname', key)
-                        
-                        self.insertObjectWithChilds(attr, childElem)
-            
-    def updateFrom(self, obj):
-        """Update object data with storage info"""
-        pass
-    
-    def updateTo(self, obj):
-        """Update storage with object info"""
-        pass
diff --git a/pyworkflow/object.py b/pyworkflow/object.py
index f2a5c958e..11afed3aa 100644
--- a/pyworkflow/object.py
+++ b/pyworkflow/object.py
@@ -35,7 +35,7 @@
 import datetime as dt
 from os.path import getmtime
 
-from pyworkflow.utils import getListFromValues, getListFromRangeString
+from pyworkflow.utils import getListFromValues, getListFromRangeString, strToDuration
 from pyworkflow.utils.reflection import getSubclasses
 
 
@@ -571,7 +571,7 @@ def evalCondition(self, condition):
         """
         # Split in possible tokens
         import re
-        tokens = re.split('\W+', condition)
+        tokens = re.split(r'\W+', condition)
         condStr = condition
         
         for t in tokens:
@@ -781,6 +781,10 @@ def getListFromRange(self):
          Useful for NumericRangeParam params"""
         return getListFromRangeString(self._objValue)
 
+    def toSeconds(self):
+
+        return strToDuration(self.get())
+
 
 class Float(Scalar):
     """Float object"""
@@ -1109,6 +1113,9 @@ class Set(Object):
     STREAM_CLOSED = 2
 
     indexes = ['_index']
+    # Dict that contains the attributes to be checked for compatibility in operations involving multiple sets,
+    # such us the union of sets. Example: {'sampling rates': 'getSamplingRate', 'dimensions': 'getDimensions'}
+    _compatibilityDict = {}
     
     def __init__(self, filename=None, prefix='', 
                  mapperClass=None, classesDict=None, **kwargs):
@@ -1125,7 +1132,6 @@ def __init__(self, filename=None, prefix='',
         self._representative = None
         self._classesDict = classesDict
         self._indexes = kwargs.get('indexes', [])
-
         # If filename is passed in the constructor, it means that
         # we want to create a new object, so we need to delete it if
         # the file exists
@@ -1133,6 +1139,10 @@ def __init__(self, filename=None, prefix='',
             self._mapperPath.set('%s, %s' % (filename, prefix))
             self.load()
 
+    @classmethod
+    def getCompatibilityDict(cls):
+        return cls._compatibilityDict
+
     def copy(self, other, copyId=True, ignoreAttrs=['_mapperPath', '_size', '_streamState']):
         """ Copies the attributes of the set
 
diff --git a/pyworkflow/plugin.py b/pyworkflow/plugin.py
index 77b7d6802..15339ab13 100644
--- a/pyworkflow/plugin.py
+++ b/pyworkflow/plugin.py
@@ -41,17 +41,20 @@
 import inspect
 import traceback
 import types
-import pkg_resources
-from email import message_from_string
-from collections import OrderedDict
 from abc import ABCMeta, abstractmethod
 
 import pyworkflow as pw
 import pyworkflow.utils as pwutils
 import pyworkflow.object as pwobj
-from pyworkflow.template import Template
+from pyworkflow.template import LocalTemplate
 from pyworkflow.utils import sortListByList
 
+try:
+    import importlib_metadata
+except ModuleNotFoundError:
+    raise ModuleNotFoundError('You are missing importlib-metadata package. '
+                              'Please run: scipion3 pip install importlib-metadata')
+
 from .constants import *
 
 
@@ -131,20 +134,21 @@ def getPlugins(cls):
     @classmethod
     def _discoverPlugins(cls):
         # Get the list of plugins registered
-        plugin_modules = []
-        for entry_point in pkg_resources.iter_entry_points('pyworkflow.plugin'):
-            plugin_modules.append(entry_point.name)
+        plugin_modules = importlib_metadata.entry_points(group='pyworkflow.plugin')
 
         # Sort the list taking into account the priority
-        plugin_modules = sortListByList(plugin_modules, pw.Config.getPriorityPackageList())
+        plugin_modules = sortListByList(plugin_modules.names, pw.Config.getPriorityPackageList())
 
         for module in plugin_modules:
             cls.registerPlugin(module)
 
     @classmethod
     def _discoverGUIPlugins(cls):
-        for entry_point in pkg_resources.iter_entry_points('pyworkflow.guiplugin'):
-            entry_point.load()
+        for entry_point in importlib_metadata.entry_points(group='pyworkflow.guiplugin'):
+            try:
+                entry_point.load()
+            except Exception as e:
+                logger.warning("Can't import %s GUI plugin: %s" % (entry_point, e))
 
     @classmethod
     def getPluginModule(cls, name):
@@ -629,7 +633,7 @@ def getSupportedVersions(cls):
     def getActiveVersion(cls, home=None, versions=None):
         """
         Returns the version of the binaries that are currently active.
-        In the current implementation it will be inferred from the \*_HOME
+        In the current implementation it will be inferred from the *_HOME
         variable, so it should contain the version number in it. """
 
         # FIXME: (JMRT) Use the basename might alleviate the issue with matching
@@ -691,7 +695,7 @@ def getTemplates(self):
         tDir = self.getPluginTemplateDir()
         if os.path.exists(tDir):
             for file in glob.glob1(tDir, "*" + SCIPION_JSON_TEMPLATES):
-                t = Template(pluginName, os.path.join(tDir, file))
+                t = LocalTemplate(pluginName, os.path.join(tDir, file))
                 tempList.append(t)
 
         return tempList
@@ -706,39 +710,3 @@ def inDevelMode(self)-> bool:
         if self._inDevelMode is None:
             self._inDevelMode = pwutils.getPythonPackagesFolder() not in self.getPath()
         return self._inDevelMode
-
-
-class PluginInfo:
-    """
-    Information related to a given plugin when it is installed via PIP
-    """
-    def __init__(self, name):
-        try:
-            dist = pkg_resources.get_distribution(name)
-            lines = [l for l in dist._get_metadata(dist.PKG_INFO)]
-            tuples = message_from_string('\n'.join(lines))
-
-        except Exception:
-            logger.info("Plugin %s seems is not a pip module yet. "
-                  "No metadata found" % name)
-            tuples = message_from_string('Author: plugin in development mode?')
-
-        self._name = name
-        self._metadata = OrderedDict()
-
-        for v in tuples.items():
-            if v[0] == 'Keywords':
-                break
-            self._metadata[v[0]] = v[1]
-
-    def getAuthor(self):
-        return self._metadata.get('Author', "")
-
-    def getAuthorEmail(self):
-        return self._metadata.get('Author-email', '')
-
-    def getHomePage(self):
-        return self._metadata.get('Home-page', '')
-
-    def getKeywords(self):
-        return self._metadata.get('Keywords', '')
diff --git a/pyworkflow/project/project.py b/pyworkflow/project/project.py
index 7e5ef80c4..ea64a0d08 100644
--- a/pyworkflow/project/project.py
+++ b/pyworkflow/project/project.py
@@ -26,9 +26,11 @@
 # **************************************************************************
 import logging
 
+from ..protocol.launch import _checkJobStatus
+
 ROOT_NODE_NAME = "PROJECT"
 logger = logging.getLogger(__name__)
-
+from pyworkflow.utils.log import LoggingConfigurator
 import datetime as dt
 import json
 import os
@@ -45,8 +47,8 @@
 from pyworkflow.mapper import SqliteMapper
 from pyworkflow.protocol.constants import (MODE_RESTART, MODE_RESUME,
                                            STATUS_INTERACTIVE, ACTIVE_STATUS,
-                                           UNKNOWN_JOBID, INITIAL_SLEEP_TIME)
-from pyworkflow.protocol.protocol import ProtImportBase
+                                           UNKNOWN_JOBID, INITIAL_SLEEP_TIME, STATUS_FINISHED)
+from pyworkflow.protocol.protocol import ProtImportBase, Protocol
 
 from . import config
 
@@ -60,8 +62,8 @@
 PROJECT_CREATION_TIME = 'CreationTime'
 
 # Regex to get numbering suffix and automatically propose runName
-REGEX_NUMBER_ENDING = re.compile('(?P<prefix>.+)(?P<number>\(\d*\))\s*$')
-REGEX_NUMBER_ENDING_CP = re.compile('(?P<prefix>.+\s\(copy)(?P<number>.*)\)\s*$')
+REGEX_NUMBER_ENDING = re.compile(r'(?P<prefix>.+)(?P<number>\(\d*\))\s*$')
+REGEX_NUMBER_ENDING_CP = re.compile(r'(?P<prefix>.+\s\(copy)(?P<number>.*)\)\s*$')
 
 
 class Project(object):
@@ -123,7 +125,7 @@ def __addPath(self, *paths):
     def getPath(self, *paths):
         """Return path from the project root"""
         if paths:
-            return os.path.join(*paths)
+            return os.path.join(*paths) # Why this is relative!!
         else:
             return self.path
 
@@ -192,6 +194,9 @@ def getTmpPath(self, *paths):
     def getLogPath(self, *paths):
         return self.getPath(PROJECT_LOGS, *paths)
 
+    def getProjectLog(self):
+        return os.path.join(self.path,self.getLogPath("project.log")) # For some reason getLogsPath is relative!
+
     def getSettings(self):
         return self.settings
 
@@ -279,6 +284,8 @@ def load(self, dbPath=None, hostsConf=None, protocolsConf=None, chdir=True,
         #     logger.info("ERROR: Project %s load failed.\n"
         #           "       Message: %s\n" % (self.path, e))
 
+    def configureLogging(self):
+        LoggingConfigurator.setUpGUILogging(self.getProjectLog())
     def _loadCreationTime(self):
         # Load creation time, it should be in project.sqlite or
         # in some old projects it is found in settings.sqlite
@@ -657,7 +664,7 @@ def scheduleProtocol(self, protocol, prerequisites=[], initialSleepTime=0):
         self.mapper.store(protocol)
         self.mapper.commit()
 
-    def _updateProtocol(self, protocol, tries=0, checkPid=False,
+    def _updateProtocol(self, protocol: Protocol, tries=0, checkPid=False,
                         skipUpdatedProtocols=True):
 
         # If this is read only exit
@@ -675,6 +682,9 @@ def _updateProtocol(self, protocol, tries=0, checkPid=False,
             if skipUpdatedProtocols:
                 # If we are already updated, comparing timestamps
                 if pwprot.isProtocolUpToDate(protocol):
+
+                    # Always check for the status of the process (queue job or pid)
+                    self.checkIsAlive(protocol)
                     return pw.NOT_UPDATED_UNNECESSARY
 
 
@@ -717,8 +727,7 @@ def _updateProtocol(self, protocol, tries=0, checkPid=False,
 
             # Check pid at the end, once updated
             if checkPid:
-                self.checkPid(protocol)
-
+                self.checkIsAlive(protocol)
 
             self.mapper.store(protocol)
 
@@ -745,6 +754,13 @@ def _updateProtocol(self, protocol, tries=0, checkPid=False,
 
         return pw.PROTOCOL_UPDATED
 
+    def checkIsAlive(self, protocol):
+        """ Check if a protocol is alive based on its jobid or pid"""
+        if protocol.getPid() == 0:
+            self.checkJobId(protocol)
+        else:
+            self.checkPid(protocol)
+
     def stopProtocol(self, protocol):
         """ Stop a running protocol """
         try:
@@ -771,12 +787,10 @@ def resetProtocol(self, protocol):
         finally:
             protocol.setSaved()
             protocol.runMode.set(MODE_RESTART)
-            protocol._store()
-            self._storeProtocol(protocol)
             protocol.makePathsAndClean()  # Create working dir if necessary
-            protocol.cleanExecutionAttributes() # Clean jobIds and Pid; otherwise, this would retain old job IDs and PIDs.
+            # Clean jobIds, Pid and StepsDone;
+            protocol.cleanExecutionAttributes()  # otherwise, this would retain old executions info
             protocol._store()
-            self._storeProtocol(protocol)
 
     def continueProtocol(self, protocol):
         """ This function should be called 
@@ -1590,7 +1604,6 @@ def checkPid(self, protocol):
         # NOTE: This may be happening even with successfully finished protocols
         # which PID is gone.
         if (protocol.isActive() and not protocol.isInteractive() and _runsLocally(protocol)
-            and not protocol.useQueue()
                 and not pwutils.isProcessAlive(pid)):
             protocol.setFailed("Process %s not found running on the machine. "
                                "It probably has died or been killed without "
@@ -1598,6 +1611,32 @@ def checkPid(self, protocol):
                                "have information about what happened to this "
                                "process." % pid)
 
+    def checkJobId(self, protocol):
+        """ Check if a running protocol is still alive or not.
+        The check will only be done for protocols that have been sent
+        to a queue system.
+        """
+        jobid = protocol.getJobIds()[0]
+        hostConfig = protocol.getHostConfig()
+
+        if jobid == UNKNOWN_JOBID:
+            return
+
+        # Include running and scheduling ones
+        # Exclude interactive protocols
+        # NOTE: This may be happening even with successfully finished protocols
+        # which PID is gone.
+        if protocol.isActive() and not protocol.isInteractive():
+
+            jobStatus = _checkJobStatus(hostConfig, jobid)
+
+            if jobStatus == STATUS_FINISHED:
+                protocol.setFailed("Process %s not found running on the machine. "
+                                   "It probably has died or been killed without "
+                                   "reporting the status to Scipion. Logs might "
+                                   "have information about what happened to this "
+                                   "process." % jobid)
+
     def iterSubclasses(self, classesName, objectFilter=None):
         """ Retrieve all objects from the project that are instances
             of any of the classes in classesName list.
@@ -1940,7 +1979,7 @@ def cleanProjectName(projectName):
         Use it whenever you want to get the final project name pyworkflow will end up.
         Spaces will be replaced by _ """
 
-        return re.sub("[^\w\d\-\_]", "-", projectName)
+        return re.sub(r"[^\w\d\-\_]", "-", projectName)
 
 
 class MissingProjectDbException(Exception):
diff --git a/pyworkflow/project/scripts/config.py b/pyworkflow/project/scripts/config.py
index 8f9eaf8a2..4e52af132 100755
--- a/pyworkflow/project/scripts/config.py
+++ b/pyworkflow/project/scripts/config.py
@@ -38,7 +38,7 @@ def usage(error):
     print("""
     ERROR: %s
     
-    Usage: scipion python -m pyworkflow.project.scripts.config_projects ProjectName [readOnly=True|False] [lifeTime=X|None]
+    Usage: scipion python -m pyworkflow.project.scripts.config ProjectName [readOnly=True|False] [lifeTime=X|None]
         This script shows (or edits) some of the configuration of the project.
         Use readOnly=True (or False) to set/unset read-only property.
         Use lifeTime=X for setting X hours or None to unset lifetime of the project.
diff --git a/pyworkflow/project/scripts/fix_links.py b/pyworkflow/project/scripts/fix_links.py
index e02b0f82d..18fc4af12 100755
--- a/pyworkflow/project/scripts/fix_links.py
+++ b/pyworkflow/project/scripts/fix_links.py
@@ -17,27 +17,23 @@ def usage(error):
         PROJECT: provide the project name to fix broken links in the imports
         SEARCH_DIR: provide a directory where to look for the files and fix the links    
     """ % error)
-    os._exit(1)
+    sys.exit(1)
 
-def main():
-    if len(sys.argv) != 3:
-        usage("Incorrect number of input parameters")
 
-    projName = sys.argv[1]
-    searchDir = os.path.abspath(sys.argv[2])
+if len(sys.argv) != 3:
+    usage("Incorrect number of input parameters")
 
-    # Create a new project
-    manager = Manager()
+projName = sys.argv[1]
+searchDir = os.path.abspath(sys.argv[2])
 
-    if not manager.hasProject(projName):
-        usage("Nonexistent project: %s" % pwutils.red(projName))
+# Create a new project
+manager = Manager()
 
-    if not os.path.exists(searchDir):
-        usage("Nonexistent SEARCH_DIR: %s" % pwutils.red(searchDir))
+if not manager.hasProject(projName):
+    usage("Nonexistent project: %s" % pwutils.red(projName))
 
-    project = manager.loadProject(projName)
+if not os.path.exists(searchDir):
+    usage("Nonexistent SEARCH_DIR: %s" % pwutils.red(searchDir))
 
-    project.fixLinks(searchDir)
-
-if __name__ == '__main__':
-    main()
+project = manager.loadProject(projName)
+project.fixLinks(searchDir)
diff --git a/pyworkflow/project/scripts/refresh.py b/pyworkflow/project/scripts/refresh.py
index b136845ad..785748b45 100644
--- a/pyworkflow/project/scripts/refresh.py
+++ b/pyworkflow/project/scripts/refresh.py
@@ -39,11 +39,11 @@ def usage(error):
     print("""
     ERROR: %s
 
-    Usage: scipion python -m scripts.project.refresh_project project_name [refresh_period]
+    Usage: scipion python -m pyworkflow.scripts.project.refresh project_name [refresh_period]
         This script will refresh the project.sqlite file of the specified project.
         We can specify how often it will refresh in seconds (defaults 60).
         e.g.
-        scipion python -m scripts.project.refresh_project TestExtractParticles 15
+        scipion python -m pyworkflow.scripts.project.refresh TestExtractParticles 15
     """ % error)
     sys.exit(1)
 
@@ -76,9 +76,8 @@ def usage(error):
 except:
     projectPath = manager.getProjectPath(projName)
 
-project = Project(path=projectPath)
+project = Project(pw.Config.getDomain(), projectPath)
 project.load()
 while True:
     runs = project.getRuns()
     sleep(wait)
-
diff --git a/pyworkflow/project/scripts/schedule.py b/pyworkflow/project/scripts/schedule.py
index 4845c6a23..f8e7e93fa 100644
--- a/pyworkflow/project/scripts/schedule.py
+++ b/pyworkflow/project/scripts/schedule.py
@@ -95,13 +95,13 @@ def usage(error):
     for child in root.getChilds():
         workflow, _ = project._getSubworkflow(child.run)
         for prot, level in workflow.values():
+            protLabelName = prot.getObjLabel()
             if prot.forceSchedule.get():
                 print(pwutils.blueStr("\nStopping scheduling at '%s'. Force scheduling found.\n"
                                       % protLabelName))
                 sys.exit()
 
             protClassName = prot.getClassName()
-            protLabelName = prot.getObjLabel()
             if (protClassName not in sys.argv[3:] and
                     protLabelName not in sys.argv[3:]):
                 project.scheduleProtocol(prot,
diff --git a/pyworkflow/project/scripts/stack2volume.py b/pyworkflow/project/scripts/stack2volume.py
index a5569567d..f461c4cb9 100755
--- a/pyworkflow/project/scripts/stack2volume.py
+++ b/pyworkflow/project/scripts/stack2volume.py
@@ -38,4 +38,4 @@ def usage(error):
         header.writeHeader()
 
     except Exception as e:
-        print(pwutils.red("Failed to change header: % s" % e))
+        print(pwutils.red("Failed to change header: %s" % e))
diff --git a/pyworkflow/project/scripts/stop.py b/pyworkflow/project/scripts/stop.py
index f7a04d816..ef1e26478 100644
--- a/pyworkflow/project/scripts/stop.py
+++ b/pyworkflow/project/scripts/stop.py
@@ -66,13 +66,13 @@ def usage(error):
 except:
     projectPath = manager.getProjectPath(projName)
 
-project = Project(path=projectPath)
+project = Project(pw.Config.getDomain(), projectPath)
 project.load()
 
 runs = project.getRuns()
 
 # Now assuming that there is no dependencies between runs
-# and the graph is lineal
+# and the graph is linear
 for prot in runs:
     if prot.isActive():
         try:
diff --git a/pyworkflow/protocol/executor.py b/pyworkflow/protocol/executor.py
index 95489d284..586bb6407 100644
--- a/pyworkflow/protocol/executor.py
+++ b/pyworkflow/protocol/executor.py
@@ -36,14 +36,12 @@
 import datetime
 import threading
 import os
-import re
-from subprocess import Popen, PIPE
 
 import pyworkflow.utils.process as process
 from pyworkflow.utils.path import getParentFolder, removeExt
 from . import constants as cts
 
-from .launch import _submit, UNKNOWN_JOBID
+from .launch import _submit, UNKNOWN_JOBID, _checkJobStatus
 
 
 class StepExecutor:
@@ -63,14 +61,14 @@ def setProtocol(self, protocol):
 
     def runJob(self, log, programName, params,           
                numberOfMpi=1, numberOfThreads=1,
-               env=None, cwd=None):
+               env=None, cwd=None, executable=None):
         """ This function is a wrapper around runJob, 
         providing the host configuration. 
         """
         process.runJob(log, programName, params,
                        numberOfMpi, numberOfThreads, 
                        self.hostConfig,
-                       env=env, cwd=cwd, gpuList=self.getGpuList())
+                       env=env, cwd=cwd, gpuList=self.getGpuList(), executable=executable)
         
     def _getRunnable(self, steps, n=1):
         """ Return the n steps that are 'new' and all its
@@ -191,7 +189,7 @@ def __init__(self, hostConfig, nThreads, **kwargs):
     def getGpuList(self):
         """ Return the GPU list assigned to current thread
         or empty list if not using GPUs. """
-        return self.gpuDict.get(threading.currentThread().thId, [])
+        return self.gpuDict.get(threading.current_thread().thId, [])
         
     def runSteps(self, steps, 
                  stepStartedCallback, 
@@ -299,7 +297,7 @@ def renameGpuIds(self):
 
         logger.debug("Updated gpus ids rebase starting from 0: %s per thread" %self.gpuDict)
 
-    def runJob(self, log, programName, params, numberOfMpi=1, numberOfThreads=1, env=None, cwd=None):
+    def runJob(self, log, programName, params, numberOfMpi=1, numberOfThreads=1, env=None, cwd=None, executable=None):
         threadId = threading.current_thread().thId
         submitDict = dict(self.hostConfig.getQueuesDefault())
         submitDict.update(self.submitDict)
@@ -313,8 +311,7 @@ def runJob(self, log, programName, params, numberOfMpi=1, numberOfThreads=1, env
         submitDict['JOB_LOGS'] = os.path.join(getParentFolder(submitDict['JOB_SCRIPT']), submitDict['JOB_NAME'])
 
         jobid = _submit(self.hostConfig, submitDict, cwd, env)
-        # if self.protocol._lock: # Just in case we detect concurrency problem
-        self.protocol.appendJobId(jobid) # append active jobs
+        self.protocol.appendJobId(jobid)  # append active jobs
         self.protocol._store(self.protocol._jobId)
 
         if (jobid is None) or (jobid == UNKNOWN_JOBID):
@@ -326,39 +323,12 @@ def runJob(self, log, programName, params, numberOfMpi=1, numberOfThreads=1, env
 
         # Check status while job running
         # REVIEW this to minimize the overhead in time put by this delay check
-        while self._checkJobStatus(self.hostConfig, jobid) == cts.STATUS_RUNNING:
+        while _checkJobStatus(self.hostConfig, jobid) == cts.STATUS_RUNNING:
             time.sleep(wait)
             if wait < 300:
                 wait += 3
 
-        # if self.protocol._lock: # Just in case we detect concurrency problem
-        self.protocol.removeJobId(jobid) # After completion, remove inactive jobs.
+        self.protocol.removeJobId(jobid)  # After completion, remove inactive jobs.
         self.protocol._store(self.protocol._jobId)
 
         return status
-
-    def _checkJobStatus(self, hostConfig, jobid):
-        command = hostConfig.getCheckCommand() % {"JOB_ID": jobid}
-        logger.debug("checking job status for %s: %s" %(jobid, command))
-        p = Popen(command, shell=True, stdout=PIPE, preexec_fn=os.setsid)
-
-        out = p.communicate()[0].decode(errors='backslashreplace')
-
-        jobDoneRegex = hostConfig.getJobDoneRegex()
-        logger.debug("Queue engine replied %s, variable JOB_DONE_REGEX has %s" %(out, jobDoneRegex))
-        # If nothing is returned we assume job is no longer in queue and thus finished
-        if out == "":
-            logger.warning("Empty response from queue system to job (%s)" %jobid)
-            return cts.STATUS_FINISHED
-        # If some string is returned we use the JOB_DONE_REGEX variable (if present) to infer the status
-        elif jobDoneRegex is not None:
-            s = re.search(jobDoneRegex, out)
-            if s:
-                logger.debug("Job (%s) finished" %jobid)
-                return cts.STATUS_FINISHED
-            else:
-                logger.debug("Job (%s) still running" %jobid)
-                return cts.STATUS_RUNNING
-        # If JOB_DONE_REGEX is not defined and queue has returned something we assume that job is still running
-        else:
-            return cts.STATUS_RUNNING
diff --git a/pyworkflow/protocol/launch.py b/pyworkflow/protocol/launch.py
index 1f03f7a79..e22e3f607 100644
--- a/pyworkflow/protocol/launch.py
+++ b/pyworkflow/protocol/launch.py
@@ -40,7 +40,7 @@
 from pyworkflow.exceptions import PyworkflowException
 from pyworkflow.utils import (redStr, greenStr, makeFilePath, join, process,
                               getHostFullName)
-from pyworkflow.protocol.constants import UNKNOWN_JOBID, STATUS_FAILED
+from pyworkflow.protocol.constants import UNKNOWN_JOBID, STATUS_FAILED, STATUS_FINISHED, STATUS_RUNNING
 
 
 # ******************************************************************
@@ -149,6 +149,7 @@ def _launchLocal(protocol, wait, stdin=None, stdout=None, stderr=None):
             protocol.setStatus(STATUS_FAILED)
         else:
             protocol.setJobId(jobId)
+            protocol.setPid(0)  # we go through the queue, so we rely on the jobId
     else:  # If not, retrieve and set the process ID (both for normal execution or when using the queue for steps)
         pId = _run(command, wait, stdin, stdout, stderr)
         protocol.setPid(pId)
@@ -216,7 +217,7 @@ def _submit(hostConfig, submitDict, cwd=None, env=None):
     out = p.communicate()[0]
     # Try to parse the result of qsub, searching for a number (jobId)
     # Review this, seems to exclusive to torque batch system
-    s = re.search('(\d+)', str(out))
+    s = re.search(r'(\d+)', str(out))
     if p.returncode == 0 and s:
         job = int(s.group(0))
         logger.info("Launched job with id %s" % job)
@@ -225,14 +226,45 @@ def _submit(hostConfig, submitDict, cwd=None, env=None):
         logger.info("Couldn't submit to queue for reason: %s " % redStr(out.decode()))
         return UNKNOWN_JOBID
 
+def _checkJobStatus(hostConfig, jobid):
+    """
+    General method to verify the job status in the queue based on the jobId and host.conf CHECK_COMMAND
+    returns: STATUS_FINISHED (finished) or STATUS_RUNNING (running)
+    """
+    command = hostConfig.getCheckCommand() % {"JOB_ID": jobid}
+    logger.debug("checking job status for %s: %s" % (jobid, command))
+
+    p = Popen(command, shell=True, stdout=PIPE, preexec_fn=os.setsid)
+
+    out = p.communicate()[0].decode(errors='backslashreplace')
+
+    jobDoneRegex = hostConfig.getJobDoneRegex()
+    logger.debug("Queue engine replied %s, variable JOB_DONE_REGEX has %s" % (out, jobDoneRegex))
+    # If nothing is returned we assume job is no longer in queue and thus finished
+    if out == "":
+        logger.warning("Empty response from queue system to job (%s)" % jobid)
+        return STATUS_FINISHED
+
+    # If some string is returned we use the JOB_DONE_REGEX variable (if present) to infer the status
+    elif jobDoneRegex is not None:
+        s = re.search(jobDoneRegex, out)
+        if s:
+            logger.debug("Job (%s) finished" % jobid)
+            return STATUS_FINISHED
+        else:
+            logger.debug("Job (%s) still running" % jobid)
+            return STATUS_RUNNING
+    # If JOB_DONE_REGEX is not defined and queue has returned something we assume that job is still running
+    else:
+        return STATUS_RUNNING
 
 def _run(command, wait, stdin=None, stdout=None, stderr=None):
     """ Execute a command in a subprocess and return the pid. """
     gcmd = greenStr(command)
     logger.info("** Running command: '%s'" % gcmd)
     p = Popen(command, shell=True, stdout=stdout, stderr=stderr)
-    jobId = p.pid
+    pid = p.pid
     if wait:
         p.wait()
 
-    return jobId
+    return pid
diff --git a/pyworkflow/protocol/params.py b/pyworkflow/protocol/params.py
index d341234a8..9934e1c84 100644
--- a/pyworkflow/protocol/params.py
+++ b/pyworkflow/protocol/params.py
@@ -244,7 +244,7 @@ def addHidden(self, *args, **kwargs):
     def _analizeCondition(self, paramName, param):
         if param.hasCondition():
             param._conditionParams = []
-            tokens = re.split('\W+', param.condition.get())
+            tokens = re.split(r'\W+', param.condition.get())
             for t in tokens:
                 if self.hasParam(t):
                     self.getParam(t)._dependants.append(paramName)
diff --git a/pyworkflow/protocol/protocol.py b/pyworkflow/protocol/protocol.py
index 42f1563ff..c604644c5 100644
--- a/pyworkflow/protocol/protocol.py
+++ b/pyworkflow/protocol/protocol.py
@@ -25,10 +25,12 @@
 This modules contains classes required for the workflow
 execution and tracking like: Step and Protocol
 """
+import contextlib
 import sys, os
 import json
 import threading
 import time
+from datetime import datetime
 
 import pyworkflow as pw
 from pyworkflow.exceptions import ValidationException, PyworkflowException
@@ -420,7 +422,7 @@ def __init__(self, **kwargs):
         # and queue parameters (only meaningful if _useQueue=True)
         self._queueParams = String()
         self.queueShown = False
-        self._jobId = CsvList() # Store queue job ids
+        self._jobId = CsvList()  # Store queue job ids
         self._pid = Integer()
         self._stepsExecutor = None
         self._stepsDone = Integer(0)
@@ -438,7 +440,8 @@ def __init__(self, **kwargs):
         # Store warnings here
         self.summaryWarnings = []
         # Get a lock for threading execution
-        self._lock = threading.Lock()
+        self._lock = threading.RLock()  # Recursive locks allows a thread to acquire lock on same object more
+        # than one time, thus avoiding deadlock situation. This fixed the concurrency problems we had before.
         self.forceSchedule = Boolean(False)
 
     def _storeAttributes(self, attrList, attrDict):
@@ -956,7 +959,7 @@ def _store(self, *objs):
         If not objects are passed, the whole protocol is stored.
         """
         if self.mapper is not None:
-            with self._lock:
+            with self._lock:  # _lock is now a Rlock object (recursive locks)
                 if len(objs) == 0:
                     self.mapper.store(self)
                 else:
@@ -1524,16 +1527,22 @@ def run(self):
                                                                project_name=self.getProject().getName(),
                                                                prot_id=self.getObjId(),
                                                                prot_name=self.getClassName()))
+
+            self.setHostFullName(pwutils.getHostFullName())
+            self.info('Hostname: %s' % self.getHostFullName())
+
             # Store the full machine name where the protocol is running
             # and also its PID
-            self.setPid(os.getpid())
-            self.setHostFullName(pwutils.getHostFullName())
+            if not self.useQueueForProtocol():  # Take as reference the pID
+                self.setPid(os.getpid())
+                self.info('PID: %s' % self.getPid())
+            else:  # Take as reference the jobID
+                self.info('Executing through the queue system')
+                self.info('JOBID: %s' % self.getJobIds())
 
-            self.info('Hostname: %s' % self.getHostFullName())
-            self.info('PID: %s' % self.getPid())
             self.info('pyworkflow: %s' % pw.__version__)
             plugin = self.getPlugin()
-            self.info('plugin: %s' %  plugin.getName())
+            self.info('plugin: %s - %s' % (plugin.getName(), plugin.getUrl()))
             package = self.getClassPackage()
             if hasattr(package, "__version__"):
                 self.info('plugin v: %s%s' %(package.__version__, ' (devel)' if plugin.inDevelMode() else '(production)'))
@@ -2363,6 +2372,7 @@ def cleanExecutionAttributes(self):
         """ Clean all the executions attributes """
         self.setPid(0)
         self._jobId.clear()
+        self._stepsDone.set(0)
 
 class LegacyProtocol(Protocol):
     """ Special subclass of Protocol to be used when a protocol class
@@ -2508,7 +2518,12 @@ def __init__(self, **kwargs):
         self.stepsExecutionMode = STEPS_PARALLEL
     def _insertAllSteps(self):
         # Insert the step that generates the steps
-        self._insertFunctionStep(self.stepsGeneratorStep)
+        self._insertFunctionStep(self.resumableStepGeneratorStep, str(datetime.now()))
+
+    def resumableStepGeneratorStep(self, ts):
+        """ This allow to resume protocols. ts is the time stamp so this stap is alway different form previous exceution"""
+        self.stepsGeneratorStep()
+
 
     def _stepsCheck(self):
 
diff --git a/pyworkflow/template.py b/pyworkflow/template.py
index 762c78d56..bf0019f50 100644
--- a/pyworkflow/template.py
+++ b/pyworkflow/template.py
@@ -6,75 +6,47 @@
 from datetime import datetime
 from pyworkflow import SCIPION_JSON_TEMPLATES, Config, VarTypes
 from pyworkflow.utils import greenStr
-
+import logging
+logger = logging.getLogger(__name__)
 
 class Template:
-    def __init__(self, pluginName, tempPath):
-        self.pluginName = pluginName
+    def __init__(self, source, name, description):
+        self.source = source
         # Tidy up templates names: removing .json.template and .json (when passed as parameter)
-        self.templateName = os.path.basename(tempPath).replace(SCIPION_JSON_TEMPLATES, "").replace(".json", "")
-        self.templatePath = os.path.abspath(tempPath)
-        self.description, self.content = self._parseTemplate()
+        self.name = name
+        self.description = description
+        self.content = None
         self.params = None
         self.projectName = None
 
     def __str__(self):
-        return self.templatePath
-
-    def getObjId(self):
-        return self.pluginName + '-' + self.templateName
-
-    def genProjectName(self):
-        self.projectName = self.getObjId() + '-' + datetime.now().strftime("%y%m%d-%H%M%S")
+        return self.name
 
-    def replaceEnvVariables(self):
-        self.content = (self.content % os.environ).split('~')
+    def getContent(self):
+        """ Returns the content of the template if not present it calls , loadContent"""
 
-    def _parseTemplate(self):
-        with open(self.templatePath, 'r') as myFile:
-            allContents = myFile.read().splitlines()
-            description, index = Template.getDescription(allContents)
+        if self.content is None:
+            self.content = self.loadContent()
 
-        if not description:
-            description = 'Not provided'
-        content = ''.join(allContents[index:])
-        return description, content
+        return self.content
 
-    @staticmethod
-    def getDescription(strList):
-        # Example of json.template file with description:
-        # -----------------------------------------------
-        # Here goes the description
-        # Description
-        # Another description line...
-        # [
-        #    {
-        #       "object.className": "ProtImportMovies",
-        #       "object.id": "2",...
-        # -----------------------------------------------
+    def loadContent(self):
+        """ Method to load into self.content the content of a template"""
+        pass
 
-        contents_start_1 = '['
-        contents_start_2 = '{'
-        description = []
-        counter = 0
-        nLines = len(strList)
+    def getObjId(self):
+        return self.source + '-' + self.name
 
-        while counter + 1 < nLines:
-            currentLine = strList[counter]
-            nextLine = strList[counter + 1]
-            if contents_start_1 not in currentLine:
-                description.append(currentLine)
-            else:
-                if contents_start_2 in nextLine:
-                    break
-                else:
-                    description.append(currentLine)
-            counter += 1
+    def genProjectName(self):
+        self.projectName = self.getObjId() + '-' + datetime.now().strftime("%y%m%d-%H%M%S")
 
-        return ''.join(description), counter
+    def replaceEnvVariables(self):
+        self.content = (self.getContent() % os.environ).split('~')
 
     def parseContent(self):
 
+        content = self.getContent()
+
         def paramStr2Param(fieldIndex, fieldString):
             fieldLst = fieldString.split('|')
 
@@ -87,8 +59,8 @@ def paramStr2Param(fieldIndex, fieldString):
 
         # Fill each field in the template in order to prevent spreading in the form
         self.params = collections.OrderedDict()
-        for index in range(1, len(self.content), 2):
-            param = paramStr2Param(index, self.content[index])
+        for index in range(1, len(content), 2):
+            param = paramStr2Param(index, content[index])
             self.params[param.getTitle()] = param
 
     def createTemplateFile(self):
@@ -98,7 +70,7 @@ def createTemplateFile(self):
 
         self._replaceFields()
 
-        finalJson = "".join(self.content)
+        finalJson = "".join(self.getContent())
 
         os.write(fileHandle, finalJson.encode())
         os.close(fileHandle)
@@ -132,6 +104,61 @@ def setParamValue(self, alias, newValue):
             raise Exception("Alias %s not recognized." % alias)
         return paramsSetted
 
+class LocalTemplate(Template):
+    """ Local template representing a json file in the file system"""
+
+    def __init__(self, source, tempPath):
+        # Tidy up templates names: removing .json.template and .json (when passed as parameter)
+        name = os.path.basename(tempPath).replace(SCIPION_JSON_TEMPLATES, "").replace(".json", "")
+        super().__init__(source,name, "")
+        self.templatePath = os.path.abspath(tempPath)
+        self.description, self.content = self._parseTemplate()
+
+    def _parseTemplate(self):
+        with open(self.templatePath, 'r') as myFile:
+            allContents = myFile.read().splitlines()
+            description, index = LocalTemplate.getDescription(allContents)
+
+        if not description:
+            description = 'Not provided'
+        content = ''.join(allContents[index:])
+        return description, content
+
+
+    @staticmethod
+    def getDescription(strList):
+        # Example of json.template file with description:
+        # -----------------------------------------------
+        # Here goes the description
+        # Description
+        # Another description line...
+        # [
+        #    {
+        #       "object.className": "ProtImportMovies",
+        #       "object.id": "2",...
+        # -----------------------------------------------
+
+        contents_start_1 = '['
+        contents_start_2 = '{'
+        description = []
+        counter = 0
+        nLines = len(strList)
+
+        while counter + 1 < nLines:
+            currentLine = strList[counter]
+            nextLine = strList[counter + 1]
+            if contents_start_1 not in currentLine:
+                description.append(currentLine)
+            else:
+                if contents_start_2 in nextLine:
+                    break
+                else:
+                    description.append(currentLine)
+            counter += 1
+
+        return ''.join(description), counter
+
+
 
 class TemplateParam(object):
     def __init__(self, index, title, value=None, varType=None, alias=None):
@@ -175,7 +202,7 @@ def check(cls, value, fieldType):
             return cls.validDecimal(value)
         elif fieldType == VarTypes.INTEGER.value:
             return cls.validInteger(value)
-        elif fieldType == VarTypes.PATH.value:
+        elif fieldType in (VarTypes.PATH.value, VarTypes.FOLDER.value):
             return cls.validPath(value)
         elif fieldType == VarTypes.STRING.value:
             return cls.validString(value)
@@ -228,7 +255,7 @@ def genFromStrList(self, templateList):
         for t in templateList:
             parsedPath = t.split(os.path.sep)
             pluginName = parsedPath[parsedPath.index('templates') - 1]
-            self.addTemplate(Template(pluginName, t))
+            self.addTemplate(LocalTemplate(pluginName, t))
 
     def sortListByPluginName(self):
         # Create a identifier with both plugin and template names to sort by both
@@ -238,12 +265,23 @@ def sortListByPluginName(self):
         return self
 
     def addScipionTemplates(self, tempId=None):
+        """ Adds scipion templates from local file system or from workflow hub.
+        :param tempId: identifier of the template to look up for. If fount only this template is chosen
+        """
+
+        self.addLocalTemplates(tempId)
+
+        if tempId is None or len(self.templates) == 0:
+            self.addWHTemplates(tempId)
+
+
+    def addLocalTemplates(self, tempId=None):
         # Check if there is any .json.template in the template folder
         # get the template folder (we only want it to be included once)
         templateFolder = Config.getExternalJsonTemplates()
         for templateName in glob.glob1(templateFolder,
                                        "*" + SCIPION_JSON_TEMPLATES):
-            t = Template("local", os.path.join(templateFolder, templateName))
+            t = LocalTemplate("local", os.path.join(templateFolder, templateName))
             if tempId is not None:
                 if t.getObjId() == tempId:
                     self.addTemplate(t)
@@ -251,6 +289,18 @@ def addScipionTemplates(self, tempId=None):
             else:
                 self.addTemplate(t)
 
+    def addWHTemplates(self, tempId=None):
+
+        try:
+
+            from pyworkflow.webservices.workflowhub import get_wh_templates
+
+            templates = get_wh_templates(tempId)
+
+            self.templates.extend(templates)
+        except Exception as e:
+            logger.warning("Can't get templates from workflow hub: %s" % e)
+
     def addPluginTemplates(self, tempId=None):
         """
         Get the templates provided by all plugins.
diff --git a/pyworkflow/utils/process.py b/pyworkflow/utils/process.py
index 1ffae0f7a..ca00833d9 100644
--- a/pyworkflow/utils/process.py
+++ b/pyworkflow/utils/process.py
@@ -42,7 +42,7 @@
 # The job should be launched from the working directory!
 def runJob(log, programname, params,           
            numberOfMpi=1, numberOfThreads=1, 
-           hostConfig=None, env=None, cwd=None, gpuList=None):
+           hostConfig=None, env=None, cwd=None, gpuList=None, executable=None):
 
     command = buildRunCommand(programname, params, numberOfMpi, hostConfig,
                               env, gpuList=gpuList)
@@ -53,10 +53,10 @@ def runJob(log, programname, params,
     log.info("** Running command: **")
     log.info(greenStr(command))
 
-    return runCommand(command, env, cwd)
+    return runCommand(command, env=env, cwd=cwd, executable=executable)
         
 
-def runCommand(command, env=None, cwd=None):
+def runCommand(command, env=None, cwd=None, executable=None):
     """ Execute command with given environment env and directory cwd """
 
     # First let us create core dumps if in debug mode
@@ -69,7 +69,7 @@ def runCommand(command, env=None, cwd=None):
     # TODO: maybe have to set PBS_NODEFILE in case it is used by "command"
     # (useful for example with gnu parallel)
     check_call(command, shell=True, stdout=sys.stdout, stderr=sys.stderr,
-               env=env, cwd=cwd)
+               env=env, cwd=cwd, executable=executable)
     # It would be nice to avoid shell=True and calling buildRunCommand()...
 
     
diff --git a/pyworkflow/utils/properties.py b/pyworkflow/utils/properties.py
index 5b74a4c67..51e4c4041 100644
--- a/pyworkflow/utils/properties.py
+++ b/pyworkflow/utils/properties.py
@@ -66,8 +66,7 @@ class Message:
     LABEL_PROTTREE_NONE = 'None'
 
     # -- Toolbar --
-    LABEL_NEW = 'New'
-    LABEL_NEW_ACTION = 'New     '
+    LABEL_NEW = 'Add'
     LABEL_EDIT = 'Edit'
     LABEL_RENAME = 'Rename '
     LABEL_COPY = 'Copy'
@@ -108,7 +107,7 @@ class Message:
     LABEL_LOGS = 'Output Logs'
     LABEL_LOGS_OUTPUT = 'Output Log'
     LABEL_LOGS_ERROR = 'Error Log'
-    LABEL_LOGS_SCIPION = 'Scipion Log'
+    LABEL_LOGS_SCIPION = 'Project Log'
     
     NO_INFO_SUMMARY = 'No summary information.'
     NO_INFO_METHODS = 'No methods information.'
diff --git a/pyworkflow/utils/utils.py b/pyworkflow/utils/utils.py
index 71fbba47a..85724b80f 100644
--- a/pyworkflow/utils/utils.py
+++ b/pyworkflow/utils/utils.py
@@ -29,7 +29,7 @@
 import platform
 import os
 import re
-from datetime import datetime
+from datetime import datetime, timezone
 import traceback
 import sysconfig
 
@@ -136,14 +136,11 @@ def prettyDelta(timedelta):
     """ Remove the milliseconds of the timedelta. """
     return str(timedelta).split('.')[0]
 
-class UtcConverter:
-    """ Class to make date conversions to utc"""
-    utc_delta = datetime.utcnow() - datetime.now()
 
-    def __call__(cls, t):
-        return t + cls.utc_delta
-# Use to_utc like a function: to_utc(date)
-to_utc = UtcConverter()
+def to_utc(t):
+    """ Make date conversions to utc"""
+    return datetime.fromtimestamp(t, tz=timezone.utc)
+
 
 def prettyLog(msg):
     logger.info(cyanStr(msg))
@@ -334,7 +331,7 @@ def hasAnyFileChanged(files, time):
 
 def hasFileChangedSince(file, time):
     """ Returns true if the file has changed after 'time'"""
-    modTime = datetime.datetime.fromtimestamp(getmtime(file))
+    modTime = datetime.fromtimestamp(os.path.getmtime(file))
     return time < modTime
 
 
@@ -414,12 +411,12 @@ def ansi(n, bold=False):
 HYPER_ALL = 'all'
 
 # Associated regular expressions
-PATTERN_BOLD = "(^|[\s])[*](?P<bold>[^\s*][^*]*[^\s*]|[^\s*])[*]"
+PATTERN_BOLD = r"(^|[\s])[*](?P<bold>[^\s*][^*]*[^\s*]|[^\s*])[*]"
 # PATTERN_BOLD = r"[\s]+[*]([^\s][^*]+[^\s])[*][\s]+"
-PATTERN_ITALIC = "(^|[\s])[_](?P<italic>[^\s_][^_]*[^\s_]|[^\s_])[_]"
+PATTERN_ITALIC = r"(^|[\s])[_](?P<italic>[^\s_][^_]*[^\s_]|[^\s_])[_]"
 # PATTERN_ITALIC = r"[\s]+[_]([^\s][^_]+[^\s])[_][\s]+"
-PATTERN_LINK1 = '(?P<link1>http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)'
-PATTERN_LINK2 = "[\[]{2}(?P<link2>[^\s][^\]]+[^\s])[\]][\[](?P<link2_label>[^\s][^\]]+[^\s])[\]]{2}"
+PATTERN_LINK1 = r'(?P<link1>http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)'
+PATTERN_LINK2 = r"[\[]{2}(?P<link2>[^\s][^\]]+[^\s])[\]][\[](?P<link2_label>[^\s][^\]]+[^\s])[\]]{2}"
 # __PATTERN_LINK2 should be first since it could contain __PATTERN_LINK1
 PATTERN_ALL = '|'.join([PATTERN_BOLD, PATTERN_ITALIC, PATTERN_LINK2, PATTERN_LINK1])
 
@@ -522,7 +519,7 @@ def getListFromRangeString(rangeStr):
     "2 5, 6-8" -> [2,5,6,7,8]
     """
     # Split elements by command or space
-    elements = re.split(',|\s', rangeStr)
+    elements = re.split(r',|\s', rangeStr)
     values = []
     for e in elements:
         if '-' in e:
@@ -596,13 +593,13 @@ def getListFromValues(valuesStr, length=None, caster=str):
 
 def getFloatListFromValues(valuesStr, length=None):
     """ Convert a string to a list of floats"""
-    return [float(v) for v in getListFromValues(valuesStr, length)]
+    return [v for v in getListFromValues(valuesStr, length, caster=float)]
 
 
 def getBoolListFromValues(valuesStr, length=None):
     """ Convert a string to a list of booleans"""
     from pyworkflow.object import Boolean
-    return [Boolean(value=v).get() for v in getListFromValues(valuesStr, length)]
+    return [v.get() for v in getListFromValues(valuesStr, length, caster=Boolean)]
 
 
 def getStringListFromValues(valuesStr, length=None):
diff --git a/pyworkflow/webservices/workflowhub.py b/pyworkflow/webservices/workflowhub.py
new file mode 100644
index 000000000..4705d9e3a
--- /dev/null
+++ b/pyworkflow/webservices/workflowhub.py
@@ -0,0 +1,74 @@
+import json
+from urllib.request import Request, urlopen
+from pyworkflow.template import Template
+
+
+class WHTemplate(Template):
+
+    def __init__(self, source, name, description, url):
+        super().__init__(source, name, description)
+        self.url = url
+
+    def loadContent(self):
+        """ Download the file pointer by url and read the content"""
+
+        return make_request(self.url, asJson=False)
+
+
+def make_request(url, asJson=True):
+    """ Makes a request to the url and returns the json as a dictionary"""
+
+    req = Request(url)
+    req.add_header("accept", "application/json")
+
+    with urlopen(req, timeout=1) as response:
+        if asJson:
+            data = json.load(response)
+        else:
+            html_response = response.read()
+            encoding = response.headers.get_content_charset('utf-8')
+            data = html_response.decode(encoding)
+        return data
+
+
+def get_workflow_file_url(workflow_id, version):
+    root_url = "https://workflowhub.eu/workflows/%s/git/%s/" % (workflow_id, version)
+    url = root_url + 'tree'
+
+    result = make_request(url)
+
+    for file in result["tree"]:
+        path = (file["path"])
+        if path.endswith(".json.template"):
+            return root_url + 'raw/' + path
+
+
+def get_wh_templates(template_id=None, organization="Scipion%20CNB"):
+    """ Returns a list of scipion templates available in workflow hub"""
+
+    url = "https://workflowhub.eu/ga4gh/trs/v2/tools?organization=%s" % organization
+
+    response = make_request(url)
+
+    template_list = []
+
+    for workflow in response:
+        workflow_id = workflow["id"]
+        name = workflow["name"]
+        description = workflow['description']
+        version = workflow["versions"][-1]
+        version_id = version["id"]
+        template_url = get_workflow_file_url(workflow_id, version_id)
+
+        new_template = WHTemplate("Workflow hub", name, description, template_url)
+        if template_id is None or new_template.getObjId() == template_id:
+            template_list.append(new_template)
+
+    return template_list
+
+
+if __name__ == "__main__":
+
+    templates = get_wh_templates()
+    for template in templates:
+        print(template)
diff --git a/requirements.txt b/requirements.txt
index 4c629d2e9..91ddc52d6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,12 @@
-bibtexparser<=1.2.0 # up to date on 07-2022
-psutil<=5.9.1 # # up to date on 07-2022
-configparser<=5.2.0 # # up to date on 07-2022
-matplotlib==3.3.4; python_version == '3.6'
-matplotlib==3.5.2; python_version >= '3.7' # up to date on 07-2022
-numpy<=1.23.0 # up to date on 07-2022. Careful. Xmipp is compiled agains numpy!
-pillow==8.4.0; python_version == '3.6'
-pillow==9.2.0; python_version >= '3.7' # up to date on 07-2022
-requests<=2.28.1 # up to date on 07-2022
-git+https://github.com/scipion-em/tkColorPicker@master#egg=tkcolorpicker # updated 10-2022
+bibtexparser<=1.4.1 # up to date on 11-2023
+psutil<=5.9.6 # # up to date on 11-2023
+configparser==6.0.0; python_version >= '3.8' # up to date on 11-2023
+matplotlib==3.7.3; python_version == '3.8'
+matplotlib==3.8.1; python_version >= '3.9' # up to date on 11-2023
+pillow==10.1.0; python_version >= '3.8' # up to date on 11-2023
+numpy==1.24.4; python_version == '3.8'
+numpy==1.26.1; python_version >= '3.9' # up to date on 11-2023. Careful. Xmipp is compiled against numpy!
+requests==2.31.0; python_version >= '3.8' # up to date on 11-2023
+git+https://github.com/scipion-em/tkColorPicker#egg=tkcolorpicker # updated 01-2023
 distro<=1.8 # up to date on 11-2023
+importlib-metadata<=6.8.0 # up to date on 11-2023
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 02701795a..683a56012 100644
--- a/setup.py
+++ b/setup.py
@@ -140,18 +140,18 @@
         #   3 - Alpha
         #   4 - Beta
         #   5 - Production/Stable
-        'Development Status :: 4 - Beta',
+        'Development Status :: 5 - Production/Stable',
         # Indicate who your project is intended for
         'Intended Audience :: Science/Research',
         # Pick your license as you wish
         'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
         # Specify the Python versions you support here. In particular, ensure
         # that you indicate whether you support Python 2, Python 3 or both.
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
+        'Programming Language :: Python :: 3.11',
+        'Programming Language :: Python :: 3.12',
         'Topic :: Scientific/Engineering'
     ],
 


Test patch:
diff --git a/pyworkflowtests/objects.py b/pyworkflowtests/objects.py
index b61d57f02..564c6b431 100644
--- a/pyworkflowtests/objects.py
+++ b/pyworkflowtests/objects.py
@@ -48,6 +48,12 @@ def getFiles(self):
         """ Get all filePaths """
         return None
 
+    # Maybe something to be moved to Object?
+    def __eq__(self, other):
+        if self._objValue is None:
+            return self.__class__ == other.__class__ and self._objId == other._objId
+        return self._objValue == other._objValue
+
 
 class Complex(MockObject):
     """ Simple class used for tests here. """
diff --git a/pyworkflowtests/protocols.py b/pyworkflowtests/protocols.py
index df21796f1..71ee38d0b 100644
--- a/pyworkflowtests/protocols.py
+++ b/pyworkflowtests/protocols.py
@@ -79,16 +79,18 @@ def _defineParams(self, form):
     def _insertAllSteps(self):
         n = 2
         for i in range(n):
-            self._insertFunctionStep('sleepAndOutput', 1, prerequisites=[])
+            self._insertFunctionStep(self.sleepAndOutput, 1, prerequisites=[])
 
     def sleepAndOutput(self, secs):
         self.sleepStep(secs)
 
-        outputSet = self.getOutputSet("myOutput", MockSetOfImages)
-        newImage = MockImage()
         with self._lock:
+            outputSet = self.getOutputSet("myOutput", MockSetOfImages)
+            newImage = MockImage()
+            time.sleep(1)
             outputSet.append(newImage)
             outputSet.write()
+
         self._store()
 
 
diff --git a/pyworkflowtests/tests/test_mappers.py b/pyworkflowtests/tests/test_mappers.py
index 4fbf50725..37c4c4d35 100644
--- a/pyworkflowtests/tests/test_mappers.py
+++ b/pyworkflowtests/tests/test_mappers.py
@@ -362,36 +362,6 @@ def test_emtpySet(self):
         self.assertEqual(len(items), 0)
 
 
-class TestXmlMapper(pwtests.BaseTest):
-
-    @classmethod
-    def setUpClass(cls):
-        pwtests.setupTestOutput(cls)
-
-    def test_XMLMapper(self):
-        fn = self.getOutputPath("model.xml")
-        c = Complex.createComplex()
-        mapper = pwmapper.XmlMapper(fn)
-        mapper.insert(c)
-        # write file
-        mapper.commit()
-
-        # TODO: Following is the gold Xml
-        # now we can not parse it from StringIO
-        goldXml = """
-<?xml version='1.0' encoding='utf-8'?>
-<ALL>
-  <Complex>
-    <imag>1.0</imag>
-    <real>1.0</real>
-  </Complex>
-</ALL>
-        """
-        # mapper2 = XmlMapper(fnGold, mod.Domain.getMapperDict())
-        # c2 = mapper2.selectFirst()
-        # self.assertEquals(c.imag.get(), c2.imag.get())
-
-
 class TestDataSet(pwtests.BaseTest):
     """ Some tests for DataSet implementation. """
 
diff --git a/pyworkflowtests/tests/test_object.py b/pyworkflowtests/tests/test_object.py
index 95cb449f9..d9440c4f4 100755
--- a/pyworkflowtests/tests/test_object.py
+++ b/pyworkflowtests/tests/test_object.py
@@ -23,9 +23,12 @@
 # *  e-mail address 'scipion@cnb.csic.es'
 # *
 # **************************************************************************
-
-import os
 import datetime as dt
+try:
+    from datetime import UTC  # python 3.11+
+except ImportError:
+    UTC = dt.timezone.utc
+import os
 from logging import DEBUG, lastResort
 from time import sleep
 
@@ -201,7 +204,7 @@ def test_Pointer(self):
         o.pointer = pwobj.Pointer()
         o.pointer.set(imgSet)
 
-        # This is not true anymore ans is allowed unless we see is needed
+        # This is not true anymore and is allowed unless we see is needed
         # The main reason is a boost in performance.
         # o.refC = o.pointer.get()
         # attrNames = [k for k, a in o.getAttributes()]
@@ -220,7 +223,7 @@ def test_Pointer(self):
 
         # Check that the Item 7 of the set is properly
         # retrieved by the pointer after setting the extended to 7
-        self.assertEqual(imgSet[7], o.pointer.get())
+        self.assertEqual(imgSet[7].getObjId(), o.pointer.get().getObjId())
 
         # Test the keyword arguments of Pointer constructor
         # repeat above tests with new pointer
@@ -271,7 +274,7 @@ def test_Sets(self):
             imgSet.append(img)
             if i == 4:
                 sleep(1)
-                halfTimeStamp = dt.datetime.utcnow().replace(microsecond=0)
+                halfTimeStamp = dt.datetime.now(UTC).replace(microsecond=0)
         imgSet.write()
 
         # Test size is 10
@@ -463,20 +466,19 @@ def test_ListsFunctions(self):
         from pyworkflow.utils import getListFromValues, getFloatListFromValues,\
             getBoolListFromValues
         
-        results = [('2x1 2x2 4 5', None, getListFromValues,
-                    ['1', '1', '2', '2', '4', '5']),
-                   ('2x1 2x2 4 5', None, getFloatListFromValues,
-                    [1., 1., 2., 2., 4., 5.]),
-                   ('1 2 3x3 0.5', 8, getFloatListFromValues,
-                    [1., 2., 3., 3., 3., 0.5, 0.5, 0.5]),
-                   ('3x1 3x0 1', 8, getBoolListFromValues,
-                    [True, True, True, False, False, False, True, True]),
+        results = [('2x1 2x2 4 5', getListFromValues, ['2x1', '2x2', '4', '5'], None),
+                   ('2x1 2x2 4 5', getFloatListFromValues, [1., 1., 2., 2., 4., 5.],None),
+                   ('1 2 3x3 0.5', getFloatListFromValues, [1., 2., 3., 3., 3., 0.5, 0.5, 0.5], 8),
+                   ('3x1 3x0 1', getBoolListFromValues, [True, True, True, False, False, False, True, True],8),
                    ]
-        for s, n, func, goldList in results:
-            l = func(s, length=n)
-            self.assertAlmostEqual(l, goldList)
-            if n:
-                self.assertEqual(n, len(l))
+
+        for s, func, goldList, length in results:
+
+            l = func(s, length=length)
+            for i in range(0,len(goldList)):
+                self.assertEqual(l[i], goldList[i])
+
+            self.assertEqual(len(goldList), len(l), )
                 
     def test_Environ(self):
         """ Test the Environ class with its utilities. """
diff --git a/pyworkflowtests/tests/test_utils.py b/pyworkflowtests/tests/test_utils.py
index e8f466841..6b6652bc4 100644
--- a/pyworkflowtests/tests/test_utils.py
+++ b/pyworkflowtests/tests/test_utils.py
@@ -105,8 +105,8 @@ def test_getListFromValues(self):
             '2x3, 3x4, 1' -> ['3', '3', '4', '4', '4', '1']"
         """
 
-        self._callAndAssert('1 1 2x2 4 4', ['1', '1', '2', '2', '4', '4'])
-        self._callAndAssert('2x3, 3x4, 1',['3', '3', '4', '4', '4', '1'])
+        self._callAndAssert('1 1 2x2 4 4', ['1', '1', '2x2', '4', '4'])
+        self._callAndAssert('1 1 2x2 4 4', [1, 1, 2, 2, 4, 4], caster=int)
         self._callAndAssert('2,3,4,1', [2, 3, 4, 1], caster=int)
         self._callAndAssert('2 , 3 , 4 , 1', [2, 3, 4, 1], caster=int)
         self._callAndAssert('2,3.3,4', [2.0, 3.3, 4.0], caster=float)
@@ -206,41 +206,5 @@ def test_durationstrings(self):
         self.assertEqual(70, strToDuration("1m 10s"), "String duration wrongly converted")
 
 
-class TestVariable(unittest.TestCase):
-
-    def test_operators(self):
-
-        myVar = Variable("Home")
-
-        # Join should work
-        try:
-            os.path.join(myVar, "hola")
-        except Exception:
-            self.fail("os.path.join for variables doesn't work")
-
-        # String concatenation ?
-        try:
-            myVar + "hola"
-            "hola" + myVar
-        except Exception:
-            self.fail("String concatenation with + for variables doesn't work")
-
-        myIntVar= Variable(50)
-
-
-        # Add should work
-        try:
-            myIntVar + 3
-            3 + myIntVar
-        except Exception:
-            self.fail("Adding a value to an int variable doesn't work")
-
-        # Division should work
-        try:
-            myIntVar/2
-            100/myIntVar
-
-        except Exception:
-            self.fail("Division does not work")