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

Issue:
Refactoring Restart & Continue workflow option. Allowing to continue and restart a workflow from a protocol
Closes #444 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..56c52a37e
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,48 @@
+# You need to edit FOLDER_WITH_VERSION with the folder that has the __version__ value. 
+name: Test conda-based installation with different python
+
+on:
+  push:
+    branches: [ "devel" ]
+
+env:
+  FOLDER_WITH_VERSION: pyworkflow
+  
+
+jobs:
+  build:
+    runs-on: "ubuntu-latest"
+    defaults:
+      run:
+        # Do not ignore bash profile files, required for conda activation
+        shell: bash -el {0}
+    strategy:
+      fail-fast: false
+      max-parallel: 6
+      matrix:
+        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up MPI
+      uses: mpi4py/setup-mpi@v1
+      with:
+        mpi: openmpi
+    - name: Set up Python
+      uses: actions/setup-python@v4
+      with:
+        python-version: 3.x
+    - name: Create a conda env called test with a specific python version
+      uses: conda-incubator/setup-miniconda@v2
+      with:
+        miniconda-version: "latest"
+        activate-environment: test
+        python-version: ${{ matrix.python-version }}
+        auto-activate-base: false
+    - name: Install scipion core plugins inside test env
+      run: |
+        export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
+        pip install .
+        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
diff --git a/CHANGES.txt b/CHANGES.txt
index c12fa03bd..9e601da6e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,7 +1,39 @@
+V3.1.0
+------
+users:
+  - New optional config variable: SCIPION_MAIN_COLOR. Any medium tone color name or hexadecimal should work(like in CSS)
+  - New optional config variable: SCIPION_BG_COLOR. Any light tone color name or hexadecimal should work(like in CSS)
+  - Control + Plus (numpad) will increase the font of the logs. Control + subtract will restore it.
+  - Toolbar and actions refactored: Most of them have shortcuts and appear in the toolbar without text, but have tooltips showing the shortcut
+  - Plugins in devel mode are shown with a delta suffix after the package name.
+  - Green lines in log are lighter, colored lines are back in the log.
+  - Commands in the log are "announced" by "** Running command: **"
+  - Project list window: F5 on the filter box will refresh the list.
+  - Project list window: Auto refresh on window activate when data is older than 60 seconds
+  - New actions: Copy (Control+c) and Paste (Control +v) will allow to copy the selection to and from the clipboard.
+  - File browser thumbnail: Writes if the file is a link
+  - Protocol in devel mode is shown prefixing the protocol in the box and shown in log
+  - Unused graphical gui nodes are cleaned up upon protocols deletion and graph painting (to clean up old projects)
+  - Error in host.conf for clusters better described.
+  - Fixed a bug related to organizing the workflow when importing a template(Import workflow template option)
+  - Terminal where Scipion is being launched only shows ERROR level info. Rest goes to scipion.log shown by the Scipion log menu.
+  - Scipion windows of the same process are now grouped under the same icon. Window Tooltip says Scipion framework instead of Toplevel
+  - Refactoring Restart & Continue workflow option. Allowing to continue and restart a workflow from a protocol
+
+  
+ developers:
+  - Plugin.inDevelMode(): returns true is plugin installed in devel mode.
+  - Shortcut for devel mode is now Control+D since Control+d is for duplicate (old copy)
+  - Protocol.getClassPlugin deprecated. Use instead getPlugin()
+  - Protocol._cleanExtraFiles(): optional method to clean files other than tmp folder.
+  - Config.isScipionRunning(): To distinguish from just importing or running.
+  - Set implements copy() leaving mapper_path out.
+  - TaseCase: compareSetProperties() added to compare 2 set's properties.
+  - Add debugging messages to check the job status and add renameGpuIds method in the StepQueueExecutor
+
 V3.0.32
 -------
   - Hotfix: fix links more versatile.
-
 V3.0.31
 -------
   - Hotfix: Fixed updated protocol icon
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..18c914718
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+  overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+  advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+  address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior,  harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/pyworkflow/config.py b/pyworkflow/config.py
index e66850f3f..07c3a4aef 100644
--- a/pyworkflow/config.py
+++ b/pyworkflow/config.py
@@ -1,5 +1,6 @@
 
 import logging
+
 logger = logging.getLogger(__file__)
 import ast
 import importlib
@@ -100,9 +101,16 @@ def join(self, *path):
     SCIPION_HOME = os.path.abspath(_get(SCIPION_HOME_VAR, ''))
     "Path where Scipion is installed. Other paths are based on this like SCIPION_SOFTWARE, SCIPION_TESTS,... unless specified"
 
+    # Actual SCIPION_HOME
+    SCIPION_HOME_DEFINED = _get(SCIPION_HOME_VAR, False)
+    "False if SCIPION_HOME is found in the environment"
+
     _root = Root(SCIPION_HOME)
     _join = _root.join
 
+    # Internal cached variables, use __ so they are not returned in getVars
+    __activeColor = None
+
     CONDA_ACTIVATION_CMD = _get(CONDA_ACTIVATION_CMD_VAR,'')
     "Command to activate/initialize conda itself. Do not confuse it with 'conda activate'. It should be defined at installation time. It looks like this: eval \"$(/extra/miniconda3/bin/conda shell.bash hook)\""
 
@@ -193,6 +201,12 @@ def join(self, *path):
     SCIPION_FONT_SIZE = int(_get('SCIPION_FONT_SIZE', SCIPION_DEFAULT_FONT_SIZE))
     "Size of the 'normal' font to be used in Scipion GUI. Defaults to 10."
 
+    SCIPION_MAIN_COLOR = _get('SCIPION_MAIN_COLOR', Color.MAIN_COLOR)
+    "Main color of the GUI. Background will be white, so for better contrast choose a dark color. Probably any name here will work: https://matplotlib.org/stable/gallery/color/named_colors.html"
+    SCIPION_BG_COLOR = _get('SCIPION_BG_COLOR', Color.BG_COLOR)
+    "Main background color of the GUI. Default is white, chose a light one. Probably any name here will work: https://matplotlib.org/stable/gallery/color/named_colors.html"
+
+
     WIZARD_MASK_COLOR = _get('WIZARD_MASK_COLOR', '[0.125, 0.909, 0.972]')
     "Color to use in some wizards."
 
@@ -235,8 +249,7 @@ def join(self, *path):
         VIEWERS = ast.literal_eval(_get('VIEWERS', "{}"))
     except Exception as e:
         VIEWERS = {}
-        print("ERROR loading preferred viewers, VIEWERS variable will be ignored")
-        print(e)
+        logger.error("ERROR loading preferred viewers, VIEWERS variable will be ignored", exc_info=e)
 
     SCIPION_DOMAIN = _get(SCIPION_DOMAIN, None)
     SCIPION_TESTS_CMD = _get(SCIPION_TESTS_CMD, getTestsScript())
@@ -289,7 +302,7 @@ def getVars(cls):
 
     @classmethod
     def printVars(cls):
-        """ Print the variables dict, mostly for debugging. """
+        """ Print the variables' dict, mostly for debugging. """
         from .utils import prettyDict
         prettyDict(cls.getVars())
 
@@ -383,6 +396,29 @@ def colorsInTerminal(cls):
         return cls.NO_COLOR == ''
 
 
+    @classmethod
+    def getActiveColor(cls):
+        """ Returns a color lighter than the SCIPION_MAIN_COLOR"""
+
+        if cls.__activeColor is None:
+            import matplotlib.colors
+            from pyworkflow.utils import lighter, rgb_to_hex
+
+            rgb_main = matplotlib.colors.to_rgb(cls.SCIPION_MAIN_COLOR)
+            rgb_main = (rgb_main[0] * 255, rgb_main[1] * 255, rgb_main[2] * 255)
+            rgb_active = lighter(rgb_main, 0.3)
+            cls.__activeColor = rgb_to_hex(rgb_active)
+
+        return cls.__activeColor
+
+    @classmethod
+    def isScipionRunning(cls):
+        """ Returns true if this execution is understood to be running Scipion.
+        In some case, documentation inspection by sphynx or when packaging a plugin using setup.py
+        this code could be reached but is not an actual execution. This is useful for cancelling some actions
+        like registering FileHandlers or other stuff not needed when just importing modules."""
+        return cls.SCIPION_HOME_DEFINED != False
+
 # Add bindings folder to sys.path
 sys.path.append(Config.getBindingsFolder())
 
diff --git a/pyworkflow/constants.py b/pyworkflow/constants.py
index 5b3bd31d4..b7698e4cf 100644
--- a/pyworkflow/constants.py
+++ b/pyworkflow/constants.py
@@ -27,6 +27,8 @@
 """
 This modules contains constants related to Pyworkflow
 """
+from enum import Enum
+
 # This variable is useful to determinate the plugins compatibility with the
 # current Scipion core release.
 # This version does not need to change with future scipion releases
@@ -40,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.0.32'
+VERSION_3_0 = '3.1.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.
@@ -122,7 +124,52 @@
 PROJECT_DBNAME = 'project.sqlite'
 PROJECT_SETTINGS = 'settings.sqlite'
 
-
+# GUI colors
+class Color:
+    RED_COLOR = 'Firebrick'  # REMOVE when not used. Red color for background label  = #B22222
+
+    # Color agnostic constants
+    MAIN_COLOR = RED_COLOR
+    ALT_COLOR = '#EAEBEC'  # Light grey for background color in form, protocol, table header and west container
+    ALT_COLOR_2 = '#F2F2F2'  # Very light grey for odd rows, input background, etc
+    ALT_COLOR_DARK= '#6E6E6E'  # Very dark grey for project title, tubes, etc
+    BG_COLOR = 'white'
+
+    STATUS_SAVED = '#D9F1FA',
+    STATUS_LAUNCHED = '#D9F1FA',
+    STATUS_RUNNING = '#FCCE62',
+    STATUS_FINISHED = '#D2F5CB',
+    STATUS_FAILED = '#F5CCCB',
+    STATUS_INTERACTIVE = '#F3F5CB',
+    STATUS_ABORTED = '#F5CCCB',
+
+
+
+# Terminal ASCII colors and tkinter map
+# Enum with ascii code colors based on -->https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
+class StrColors(Enum):
+    gray = '30'
+    red = '31'
+    green = '32'
+    yellow = '33'
+    blue = '34'
+    magenta = '35'
+    cyan = '36'
+    white = '37'
+    lightgreen = '92'
+
+# Console (and XMIPP) escaped colors, and the related tags that we create
+# with Text.tag_config(). This dict is used in OutputText:addLine()
+ASCII_COLOR_2_TKINTER =\
+    {StrColors.gray.value: 'gray',
+     StrColors.red.value: 'red',
+     StrColors.green.value: 'yellowgreen',
+     StrColors.lightgreen.value: 'yellowgreen',
+     StrColors.yellow.value: 'yellow',
+     StrColors.blue.value: 'blue',
+     StrColors.magenta.value: 'magenta',
+     StrColors.cyan.value: 'cyan',
+     StrColors.white.value: 'white'}
 
 class DOCSITEURLS:
     """Documentation site URL useful when exceptions happens and you want to point to some pages"""
diff --git a/pyworkflow/gui/browser.py b/pyworkflow/gui/browser.py
index 07fea6608..b1073877c 100644
--- a/pyworkflow/gui/browser.py
+++ b/pyworkflow/gui/browser.py
@@ -115,7 +115,7 @@ def _fillRightTop(self, top):
         self.label.grid(row=0, column=0, sticky='news')
 
     def _fillRightBottom(self, bottom):
-        self.text = TaggedText(bottom, width=40, height=15, bg='white',
+        self.text = TaggedText(bottom, width=40, height=15, bg=Config.SCIPION_BG_COLOR,
                                takefocus=0)
         self.text.grid(row=0, column=0, sticky='news')
 
@@ -129,9 +129,11 @@ def _itemClicked(self, obj):
             if img is None:
                 img = self.noImage
             self.label.config(image=img)
+
         # Update text preview
         self.text.setReadOnly(False)
         self.text.clear()
+
         if desc is not None:
             self.text.addText(desc)
         self.text.setReadOnly(True)
@@ -282,7 +284,11 @@ def getObjectPreview(self, obj):
             for fileHandler in fileHandlers:
                 preview = fileHandler.getFilePreview(obj)
                 if preview:
-                    return preview
+                    img, desc = preview
+                    if obj.isLink():
+                        desc = "Is a link" if desc is None else desc + "\nIs a link."
+                    return img, desc
+
         except Exception as e:
             msg = "Couldn't get preview for %s" % obj
             logger.error(msg, exc_info=e)
@@ -462,7 +468,7 @@ def _fillLeftPanel(self, frame):
                                                             sticky='nw', pady=3)
             tk.Entry(entryFrame,
                      textvariable=self.entryVar,
-                     bg='white',
+                     bg=Config.SCIPION_BG_COLOR,
                      width=65,
                      font=gui.getDefaultFont()).grid(row=0, column=1, sticky='nw', pady=3)
 
diff --git a/pyworkflow/gui/canvas.py b/pyworkflow/gui/canvas.py
index 28f8dd61e..c6c8772c8 100644
--- a/pyworkflow/gui/canvas.py
+++ b/pyworkflow/gui/canvas.py
@@ -34,6 +34,7 @@
 import tkinter as tk
 import operator
 
+from pyworkflow import Config
 from pyworkflow.gui import gui, getDefaultFont, cfgFontSize
 from pyworkflow.gui.widgets import Scrollable
 
@@ -51,7 +52,7 @@ class Canvas(tk.Canvas, Scrollable):
     _images = {}
 
     def __init__(self, parent, tooltipCallback=None, tooltipDelay=1500, **kwargs):
-        defaults = {'bg': 'white'}
+        defaults = {'bg': Config.SCIPION_BG_COLOR}
         defaults.update(kwargs)
         Scrollable.__init__(self, parent, tk.Canvas, **defaults)
 
@@ -220,9 +221,19 @@ def onRightClick(self, e=None):
                     self._menu.add_separator()
                 else:
                     img = ''
-                    if len(a) > 2:  # image for the action
+                    label= a[0]
+                    size = len(a)
+
+                    if size > 2:  # image for the action
                         img = self.getImage(a[2])
-                    self._menu.add_command(label=a[0], command=a[1],
+
+                        # Shortcuts
+                        if size > 3:
+                            shortCut = a[3]
+                            if shortCut:
+                                label= "%s (%s)" % (label, shortCut)
+
+                    self._menu.add_command(label=label, command=a[1],
                                            image=img, compound=tk.LEFT,
                                            font=gui.getDefaultFont())
             self._menu.post(e.x_root, e.y_root)
diff --git a/pyworkflow/gui/dialog.py b/pyworkflow/gui/dialog.py
index c7b904471..46c37c3c7 100644
--- a/pyworkflow/gui/dialog.py
+++ b/pyworkflow/gui/dialog.py
@@ -31,7 +31,7 @@
 import tkinter as tk
 import traceback
 from tkcolorpicker import askcolor as _askColor
-
+from pyworkflow import Config
 from pyworkflow.exceptions import PyworkflowException
 from pyworkflow.utils import Message, Icon, Color
 from . import gui, Window, widgets, configureWeigths, LIST_TREEVIEW, defineStyle
@@ -97,6 +97,14 @@ def __init__(self, parent, title, **kwargs):
         bodyFrame.grid(row=0, column=0, sticky='news',
                        padx=5, pady=5)
 
+        # Frame for the info/message label
+        infoFrame = tk.Frame(self)
+        infoFrame.grid(row=1, column=0, sticky='sew',
+                      padx=5, pady=(0, 5))
+        self.floatingMessage = tk.Label(infoFrame, text="", fg=Config.SCIPION_MAIN_COLOR)
+        self.floatingMessage.grid(row=0, column=0, sticky='news')
+
+        # Create buttons
         self.icons = kwargs.get('icons',
                                 {RESULT_YES: Icon.BUTTON_SELECT,
                                  RESULT_NO: Icon.BUTTON_CLOSE,
@@ -106,10 +114,11 @@ def __init__(self, parent, title, **kwargs):
         self.buttons = kwargs.get('buttons', [('OK', RESULT_YES),
                                               ('Cancel', RESULT_CANCEL)])
         self.defaultButton = kwargs.get('default', 'OK')
+
+        # Frame for buttons
         btnFrame = tk.Frame(self)
-        # Create buttons 
         self.buttonbox(btnFrame)
-        btnFrame.grid(row=1, column=0, sticky='sew',
+        btnFrame.grid(row=2, column=0, sticky='sew',
                       padx=5, pady=(0, 5))
 
         gui.configureWeigths(self)
@@ -239,6 +248,10 @@ def resultNo(self):
     def resultCancel(self):
         return self.result == RESULT_CANCEL
 
+    def info(self, message):
+        """ Shows a info message for long running processes to inform the user GUI is not frozen"""
+        self.floatingMessage.config(text=message)
+
 
 def fillMessageText(text, message):
     # Insert lines of text
@@ -260,8 +273,8 @@ def fillMessageText(text, message):
 
 
 def createMessageBody(bodyFrame, message, image,
-                      frameBg='white',
-                      textBg='white',
+                      frameBg=Config.SCIPION_BG_COLOR,
+                      textBg=Config.SCIPION_BG_COLOR,
                       textPad=5):
     """ Create a Text containing the message.
     Params:
@@ -310,7 +323,7 @@ def body(self, bodyFrame):
         super().body(bodyFrame)
 
         def addTraceback(event):
-            detailsText = TaggedText(bodyFrame, bg='white', bd=0, highlightthickness=0)
+            detailsText = TaggedText(bodyFrame, bg=Config.SCIPION_BG_COLOR, bd=0, highlightthickness=0)
             traceStr = traceback.format_exc()
             fillMessageText(detailsText, traceStr)
             detailsText.frame.grid(row=row+1, column=0, columnspan=2, sticky='news', padx=5, pady=5)
@@ -321,12 +334,12 @@ def addTraceback(event):
 
             if isinstance(self._exception, PyworkflowException):
                 helpUrl = self._exception.getUrl()
-                labelUrl = TaggedText(bodyFrame, bg='white', bd=0, highlightthickness=0)
+                labelUrl = TaggedText(bodyFrame, bg=Config.SCIPION_BG_COLOR, bd=0, highlightthickness=0)
                 fillMessageText(labelUrl,"Please go here for more details: %s" % helpUrl)
                 labelUrl.grid(row=row, column=0, columnspan=2, sticky='news')
                 row += 1
 
-            label = tk.Label(bodyFrame, text="Show details...", bg='white', bd=0)
+            label = tk.Label(bodyFrame, text="Show details...", bg=Config.SCIPION_BG_COLOR, bd=0)
             label.grid(row=row, column=0, columnspan=2, sticky='news')
             label.bind("<Button-1>", addTraceback)
 
@@ -393,15 +406,15 @@ def __init__(self, parent, title, entryLabel, entryWidth=20,
         Dialog.__init__(self, parent, title)
 
     def body(self, bodyFrame):
-        bodyFrame.config(bg='white')
-        frame = tk.Frame(bodyFrame, bg='white')
+        bodyFrame.config(bg=Config.SCIPION_BG_COLOR)
+        frame = tk.Frame(bodyFrame, bg=Config.SCIPION_BG_COLOR)
         frame.grid(row=0, column=0, padx=20, pady=20)
         row = 0
         if self.headerLabel:
-            label = tk.Label(bodyFrame, text=self.headerLabel, bg='white', bd=0)
+            label = tk.Label(bodyFrame, text=self.headerLabel, bg=Config.SCIPION_BG_COLOR, bd=0)
             label.grid(row=row, column=0, columnspan=2, sticky='nw', padx=(15, 10), pady=15)
             row += 1
-        label = tk.Label(bodyFrame, text=self.entryLabel, bg='white', bd=0)
+        label = tk.Label(bodyFrame, text=self.entryLabel, bg=Config.SCIPION_BG_COLOR, bd=0)
         label.grid(row=row, column=0, sticky='nw', padx=(15, 10), pady=15)
         self.entry = tk.Entry(bodyFrame, bg=gui.cfgEntryBgColor,
                               width=self.entryWidth, textvariable=self.tkvalue)
@@ -438,12 +451,12 @@ def __init__(self, parent, title, obj, mapper, **kwargs):
         Dialog.__init__(self, parent, title, **kwargs)
 
     def body(self, bodyFrame):
-        bodyFrame.config(bg='white')
-        frame = tk.Frame(bodyFrame, bg='white')
+        bodyFrame.config(bg=Config.SCIPION_BG_COLOR)
+        frame = tk.Frame(bodyFrame, bg=Config.SCIPION_BG_COLOR)
         frame.grid(row=0, column=0, padx=20, pady=20)
 
         # Label
-        label_text = tk.Label(bodyFrame, text=self.labelText, bg='white', bd=0)
+        label_text = tk.Label(bodyFrame, text=self.labelText, bg=Config.SCIPION_BG_COLOR, bd=0)
         label_text.grid(row=0, column=0, sticky='nw', padx=(15, 10), pady=15)
         # Label box
         var = tk.StringVar()
@@ -452,7 +465,7 @@ def body(self, bodyFrame):
         self.textLabel.grid(row=0, column=1, sticky='news', padx=5, pady=5)
 
         # Comment
-        label_comment = tk.Label(bodyFrame, text=self.commentLabel, bg='white', bd=0)
+        label_comment = tk.Label(bodyFrame, text=self.commentLabel, bg=Config.SCIPION_BG_COLOR, bd=0)
         label_comment.grid(row=1, column=0, sticky='nw', padx=(15, 10), pady=15)
         # Comment box
         self.textComment = Text(bodyFrame, height=self.commentHeight,
@@ -629,7 +642,7 @@ def comparison():
         label = tk.Label(self.searchBoxframe, text="Filter")
         label.grid(row=0, column=0, sticky='nw')
         self._searchVar = tk.StringVar(value='')
-        self.entry = tk.Entry(self.searchBoxframe, bg='white',
+        self.entry = tk.Entry(self.searchBoxframe, bg=Config.SCIPION_BG_COLOR,
                               textvariable=self._searchVar, width=40,
                               font=gui.getDefaultFont())
 
@@ -694,12 +707,12 @@ def __init__(self, parent, title, provider,
         ListDialog.__init__(self, parent, title, provider, message, **kwargs)
 
     def body(self, bodyFrame):
-        bodyFrame.config(bg='white')
+        bodyFrame.config(bg=Config.SCIPION_BG_COLOR)
         gui.configureWeigths(bodyFrame, 1, 0)
 
         # Add an extra frame to insert the Toolbar
         # and another one for the ListDialog's body
-        self.toolbarFrame = tk.Frame(bodyFrame, bg='white')
+        self.toolbarFrame = tk.Frame(bodyFrame, bg=Config.SCIPION_BG_COLOR)
         self.toolbarFrame.grid(row=0, column=0, sticky='new')
         if self.toolbarButtons:
             for i, b in enumerate(self.toolbarButtons):
@@ -714,7 +727,7 @@ def body(self, bodyFrame):
     def _addButton(self, button, col):
         btn = tk.Label(self.toolbarFrame, text=button.text,
                        image=self.getImage(button.icon),
-                       compound=tk.LEFT, cursor='hand2', bg='white')
+                       compound=tk.LEFT, cursor='hand2', bg=Config.SCIPION_BG_COLOR)
         btn.grid(row=0, column=col, sticky='nw', padx=(5, 0), pady=(5, 0))
         btn.bind('<Button-1>', button.command)
 
@@ -748,13 +761,21 @@ def close(self):
 
 
 class FloatingMessage:
-    def __init__(self, master, msg, xPos=750, yPos=80, textWidth=280,
-                 font='Helvetica', size=12, bd=1, bg='#6E6E6E', fg='white'):
+    def __init__(self, master, msg, xPos=None, yPos=None, textWidth=280,
+                 font='Helvetica', size=12, bd=1, bg=Config.SCIPION_MAIN_COLOR, fg='white'):
+
+        if xPos is None:
+            xPos = (master.winfo_width()-textWidth)/2
+            yPos = master.winfo_height()/2
+
         self.floatingMessage = tk.Label(master, text="   %s   " % msg,
                                         bd=bd, bg=bg, fg=fg)
         self.floatingMessage.place(x=xPos, y=yPos, width=textWidth)
         self.floatingMessage.config(font=(font, size))
 
+    def setMessage(self, msg):
+        self.floatingMessage.config(text=msg)
+
     def show(self):
         self.floatingMessage.update_idletasks()
 
@@ -777,11 +798,11 @@ def __init__(self, parent, title, provider, message=None, **args):
                         buttons=[('Select', RESULT_YES), ('Cancel', RESULT_CANCEL)])
 
     def body(self, bodyFrame):
-        bodyFrame.config(bg='white')
+        bodyFrame.config(bg=Config.SCIPION_BG_COLOR)
         gui.configureWeigths(bodyFrame)
         self._createTree(bodyFrame)
         if self.message:
-            label = tk.Label(bodyFrame, text=self.message, bg='white',
+            label = tk.Label(bodyFrame, text=self.message, bg=Config.SCIPION_BG_COLOR,
                              image=self.getImage(Icon.LIGHTBULB), compound=tk.LEFT)
             label.grid(row=1, column=0, sticky='nw', padx=5, pady=5)
         self.initial_focus = self.tree
@@ -840,7 +861,7 @@ def __init__(self, parentWindow, title="Search element", onClick=None, onDoubleC
         self.onClick = self._click if onClick is None else onClick
         self.onDoubleClick = self._double_click if onDoubleClick is None else onDoubleClick
 
-        content = tk.Frame(self.root, bg='white')
+        content = tk.Frame(self.root, bg=Config.SCIPION_BG_COLOR)
         self._createContent(content)
         content.grid(row=0, column=0, sticky='news')
         content.columnconfigure(0, weight=1)
@@ -855,9 +876,9 @@ def _createContent(self, content):
 
     def _createSearchBox(self, content):
         """ Create the Frame with Search widgets """
-        frame = tk.Frame(content, bg='white')
+        frame = tk.Frame(content, bg=Config.SCIPION_BG_COLOR)
 
-        label = tk.Label(frame, text="Search", bg='white')
+        label = tk.Label(frame, text="Search", bg=Config.SCIPION_BG_COLOR)
         label.grid(row=0, column=0, sticky='nw')
         self._searchVar = tk.StringVar()
         entry = tk.Entry(frame, bg='white', textvariable=self._searchVar, font=gui.getDefaultFont())
@@ -873,7 +894,7 @@ def _createSearchBox(self, content):
         frame.grid(row=0, column=0, sticky='new', padx=5, pady=(10, 5))
 
     def _createResultsBox(self, content):
-        frame = tk.Frame(content, bg=Color.LIGHT_GREY_COLOR, padx=5, pady=5)
+        frame = tk.Frame(content, bg=Color.ALT_COLOR, padx=5, pady=5)
         configureWeigths(frame)
         self._resultsTree = self._createResultsTree(frame,
                                                     show=None,
diff --git a/pyworkflow/gui/form.py b/pyworkflow/gui/form.py
index b11e3f3b7..f1cd08c8a 100644
--- a/pyworkflow/gui/form.py
+++ b/pyworkflow/gui/form.py
@@ -42,16 +42,15 @@
 import pyworkflow.protocol as pwprot
 from pyworkflow.mapper import Mapper
 from pyworkflow.viewer import DESKTOP_TKINTER
-from pyworkflow.protocol.constants import MODE_RESTART
+from pyworkflow.protocol.constants import MODE_RESTART, MODE_RESUME
 
-from . import gui
+from . import gui, RESULT_RUN_SINGLE
 from pyworkflow.gui.project.utils import getStatusColorFromRun
 from .gui import configureWeigths, Window
 from .browser import FileBrowserWindow
 from .widgets import Button, HotButton, IconButton
 from .dialog import (showInfo, showError, showWarning, EditObjectDialog,
-                     ListDialog, askYesNo, Dialog, RESULT_CANCEL,
-                     askSingleAllCancel, RESULT_RUN_ALL, RESULT_RUN_SINGLE)
+                     ListDialog, Dialog, RESULT_CANCEL,  RESULT_RUN_ALL)
 from .canvas import Canvas
 from .tree import TreeProvider, BoundTree
 from .text import Text
@@ -900,13 +899,13 @@ def _createContent(self):
         self.canvasFrame = tk.Frame(self, name="sectioncontentframe")
         configureWeigths(self.canvasFrame)
         self.canvas = Canvas(self.canvasFrame, width=625, height=self.height,
-                             bg="white", highlightthickness=0, name="sectioncanvas")
+                             bg=pw.Config.SCIPION_BG_COLOR, highlightthickness=0, name="sectioncanvas")
         self.canvas.grid(row=0, column=0, sticky='news')
         self.canvasFrame.grid(row=1, column=0, sticky='news')
 
         configureWeigths(self.canvas)
 
-        self.contentFrame = tk.Frame(self.canvas, bg='white', bd=0,
+        self.contentFrame = tk.Frame(self.canvas, bg=pw.Config.SCIPION_BG_COLOR, bd=0,
                                      name="sectioncanvasframe")
         self.contentFrame.grid(row=1, column=0, sticky='news')
         self.contentId = self.canvas.create_window(0, 0, anchor=tk.NW,
@@ -1058,7 +1057,7 @@ def _initialize(self, showButtons):
                 self._labelFont = self.window.fontBold
             self.parent.columnconfigure(0, minsize=250)
             self.parent.columnconfigure(1, minsize=250)
-            self.btnFrame = tk.Frame(self.parent, bg='white')
+            self.btnFrame = tk.Frame(self.parent, bg=pw.Config.SCIPION_BG_COLOR)
         else:
             self.btnFrame = None
             self._labelSticky = 'ne'
@@ -1071,7 +1070,7 @@ def _getParamLabel(self):
         return self.param.label.get()
 
     def _createLabel(self):
-        bgColor = 'white'
+        bgColor = pw.Config.SCIPION_BG_COLOR
 
         if self.param.isExpert():
             bgColor = 'lightgrey'
@@ -1080,7 +1079,7 @@ def _createLabel(self):
                               bg=bgColor, font=self._labelFont, wraplength=500)
 
     def _createContent(self):
-        self.content = tk.Frame(self.parent, bg='white')
+        self.content = tk.Frame(self.parent, bg=pw.Config.SCIPION_BG_COLOR)
         gui.configureWeigths(self.content)
         # self.var should be set after this
         self._createContentWidgets(self.param, self.content)
@@ -1130,22 +1129,29 @@ def _findParamWizard(self):
         return False
 
     @staticmethod
-    def createBoolWidget(parent, **args):
+    def createBoolWidget(parent, display=pwprot.BooleanParam.DISPLAY_YES_NO, **args):
         """ Return a BoolVar associated with a yes/no selection.
         **args: extra arguments passed to tk.Radiobutton and tk.Frame
             constructors.
+
+            :param checkbox: will use a Checkbutton instead.
         """
         var = BoolVar()
         frameArgs = dict(args)
         if 'font' in frameArgs:
             del frameArgs['font']
         frame = tk.Frame(parent, **frameArgs)
-        rb1 = tk.Radiobutton(frame, text='Yes', variable=var.tkVar,
-                             highlightthickness=0, value=1, **args)
-        rb1.grid(row=0, column=0, padx=2, sticky='w')
-        rb2 = tk.Radiobutton(frame, text='No', variable=var.tkVar,
-                             highlightthickness=0, value=0, **args)
-        rb2.grid(row=0, column=1, padx=2, sticky='w')
+
+        if display == pwprot.BooleanParam.DISPLAY_CHECKBOX:
+            chk = tk.Checkbutton(frame, variable=var.tkVar, **args)
+            chk.grid(row=0, column=0, padx=2, sticky="w")
+        else:
+            rb1 = tk.Radiobutton(frame, text='Yes', variable=var.tkVar,
+                                 highlightthickness=0, value=1, **args)
+            rb1.grid(row=0, column=0, padx=2, sticky='w')
+            rb2 = tk.Radiobutton(frame, text='No', variable=var.tkVar,
+                                 highlightthickness=0, value=0, **args)
+            rb2.grid(row=0, column=1, padx=2, sticky='w')
 
         return var, frame
 
@@ -1164,7 +1170,8 @@ def _createContentWidgets(self, param, content):
             var = 0
 
         elif t is pwprot.BooleanParam:
-            var, frame = ParamWidget.createBoolWidget(content, bg='white',
+            var, frame = ParamWidget.createBoolWidget(content, display=param.display,
+                                                      bg=pw.Config.SCIPION_BG_COLOR,
                                                       font=self.window.font)
             frame.grid(row=0, column=0, sticky='w')
 
@@ -1179,15 +1186,15 @@ def _createContentWidgets(self, param, content):
                 for i, opt in enumerate(param.choices):
                     rb = tk.Radiobutton(content, text=opt, variable=var.tkVar,
                                         value=opt, font=self.window.font,
-                                        bg='white', highlightthickness=0)
+                                        bg=pw.Config.SCIPION_BG_COLOR, highlightthickness=0)
                     rb.grid(row=i, column=0, sticky='w')
             elif param.display == pwprot.EnumParam.DISPLAY_HLIST:
-                rbFrame = tk.Frame(content, bg='white')
+                rbFrame = tk.Frame(content, bg=pw.Config.SCIPION_BG_COLOR)
                 rbFrame.grid(row=0, column=0, sticky='w')
                 for i, opt in enumerate(param.choices):
                     rb = tk.Radiobutton(rbFrame, text=opt, variable=var.tkVar,
                                         value=opt, font=self.window.font,
-                                        bg='white')
+                                        bg=pw.Config.SCIPION_BG_COLOR)
                     rb.grid(row=0, column=i, sticky='w', padx=(0, 5))
             else:
                 raise Exception("Invalid display value '%s' for EnumParam"
@@ -1585,7 +1592,7 @@ def _createLabel(self):
 
     def _createContent(self):
         self.content = tk.LabelFrame(self.parent, text=self.param.getLabel(),
-                                     bg='white')
+                                     bg=pw.Config.SCIPION_BG_COLOR)
         gui.configureWeigths(self.content, column=1)
 
     def show(self):
@@ -1644,9 +1651,9 @@ def __init__(self, title, protocol, callback, master=None,
         self.protocol = protocol
         # This control when to close or not after execute
         self.visualizeMode = kwargs.get('visualizeMode', False)
-        self.headerBgColor = pwutils.Color.RED_COLOR
+        self.headerBgColor = pw.Config.SCIPION_MAIN_COLOR
         if self.visualizeMode:
-            self.headerBgColor = pwutils.Color.DARK_GREY_COLOR
+            self.headerBgColor = pwutils.Color.ALT_COLOR_DARK
         # Allow to open child protocols form (for workflows)
         self.childMode = kwargs.get('childMode', False)
         self.updateProtocolCallback = kwargs.get('updateProtocolCallback', None)
@@ -1777,7 +1784,7 @@ def _createParallel(self, runFrame, r):
                                     sticky='e', row=r, pady=0)
 
             if allowThreads or allowMpi:
-                procFrame = tk.Frame(runFrame, bg='white')
+                procFrame = tk.Frame(runFrame, bg=pw.Config.SCIPION_BG_COLOR)
                 r2 = 0
                 c2 = 0
                 sticky = 'e'
@@ -1796,11 +1803,11 @@ def _createParallel(self, runFrame, r):
                             prot.numberOfMpi.set(1)
 
                         self.procTypeVar.trace('w', self._setThreadsOrMpi)
-                        procCombo = tk.Frame(procFrame, bg='white')
+                        procCombo = tk.Frame(procFrame, bg=pw.Config.SCIPION_BG_COLOR)
                         for i, opt in enumerate([THREADS, MPI]):
                             rb = tk.Radiobutton(procCombo, text=opt,
                                                 variable=self.procTypeVar,
-                                                value=opt, bg='white',
+                                                value=opt, bg=pw.Config.SCIPION_BG_COLOR,
                                                 highlightthickness=0)
                             rb.grid(row=0, column=i, sticky='w', padx=(0, 5))
 
@@ -1851,7 +1858,7 @@ def _createParallel(self, runFrame, r):
             if allowGpu:
                 self._createHeaderLabel(runFrame, "GPU IDs", bold=True,
                                         sticky='e', row=r, column=0, pady=0)
-                gpuFrame = tk.Frame(runFrame, bg='white')
+                gpuFrame = tk.Frame(runFrame, bg=pw.Config.SCIPION_BG_COLOR)
                 gpuFrame.grid(row=r, column=1, sticky='ew', columnspan=2)
 
                 self.useGpuVar = tk.IntVar()
@@ -1862,7 +1869,7 @@ def _createParallel(self, runFrame, r):
                     for i, opt in enumerate(['Yes', 'No']):
                         rb = tk.Radiobutton(gpuFrame, text=opt,
                                             variable=self.useGpuVar,
-                                            value=1 - i, bg='white',
+                                            value=1 - i, bg=pw.Config.SCIPION_BG_COLOR,
                                             highlightthickness=0)
                         rb.grid(row=0, column=i, sticky='w', padx=(0, 5), pady=5)
 
@@ -1899,7 +1906,7 @@ def _createCommon(self, parent):
                                   headerBgColor=self.headerBgColor,
                                   name="runsection")
 
-        runFrame = tk.Frame(runSection.contentFrame, bg='white', name="runframe")
+        runFrame = tk.Frame(runSection.contentFrame, bg=pw.Config.SCIPION_BG_COLOR, name="runframe")
         runFrame.grid(row=0, column=0, sticky='new')
 
         r = 0  # Run name
@@ -1928,7 +1935,7 @@ def _createCommon(self, parent):
 
         r = 1  # Execution
 
-        modeFrame = tk.Frame(runFrame, bg='white')
+        modeFrame = tk.Frame(runFrame, bg=pw.Config.SCIPION_BG_COLOR)
 
         if not self.disableRunMode:
             self._createHeaderLabel(runFrame, pwutils.Message.LABEL_EXECUTION,
@@ -1939,7 +1946,7 @@ def _createCommon(self, parent):
                                                pwprot.MODE_CHOICES,
                                                self.protocol.runMode.get(),
                                                self._onRunModeChanged,
-                                               bg='white', font=self.font)
+                                               bg=pw.Config.SCIPION_BG_COLOR, font=self.font)
             runMode.grid(row=0, column=0, sticky='e', padx=(0, 5), pady=5)
             btnHelp = IconButton(modeFrame, pwutils.Message.TITLE_COMMENT, pwutils.Icon.ACTION_HELP,
                                  highlightthickness=0,
@@ -1969,7 +1976,7 @@ def _createCommon(self, parent):
                                 sticky='e',
                                 column=c)
 
-        var, frame = ParamWidget.createBoolWidget(runFrame, bg='white',
+        var, frame = ParamWidget.createBoolWidget(runFrame, bg=pw.Config.SCIPION_BG_COLOR,
                                                   font=self.font)
         btn = IconButton(frame, pwutils.Message.LABEL_BUTTON_WIZ, pwutils.Icon.ACTION_EDIT,
                          highlightthickness=0, command=self._editQueueParams, tooltip="Edit queue parameters")
@@ -2200,7 +2207,7 @@ def _createHeaderLabel(self, parent, text, bold=False, **gridArgs):
         font = self.font
         if bold:
             font = self.fontBold
-        label = tk.Label(parent, text=text, font=font, bg='white')
+        label = tk.Label(parent, text=text, font=font, bg=pw.Config.SCIPION_BG_COLOR)
         if gridArgs:
             gridDefaults = {'row': 0, 'column': 0, 'padx': 5, 'pady': 5}
             gridDefaults.update(gridArgs)
@@ -2242,7 +2249,6 @@ def _getQueueReady(self):
             return self._editQueueParams()
 
     def execute(self, e=None):
-
         if self.protocol.useQueue():
             if not self._getQueueReady():
                 return
@@ -2257,34 +2263,24 @@ def execute(self, e=None):
                                  "*Note*: Your system is configured with MANDATORY = %d.\n"
                                  "        This value can be changed in Scipion/config/hosts.conf" % (cores, mandatory))
                 return
-        if self.protocol.getRunMode() == MODE_RESTART:
-            protocolList = ""
-            if self.protocol.getObjId():
-                project = self.protocol.getProject()
-                workflowProtocolList, activeProtList = project._getWorkflowFromProtocol(self.protocol)
-                for prot, level in workflowProtocolList.values():
-                    protocolList += ("\n* " + prot.getRunName())
-                if len(workflowProtocolList) > 1:
-                    result = askSingleAllCancel(pwutils.Message.TITLE_RESTART_FORM,
-                                                pwutils.Message.LABEL_RESTART_FORM_MANY % ('%s\n' % protocolList),
-                                                self.root)
-                    if result == RESULT_RUN_ALL:
-                        self.protocol._store()
-                        project._storeProtocol(self.protocol)
-                        project._restartWorkflow(workflowProtocolList)
-                        self.close()
-                        return
-                    elif result == RESULT_RUN_SINGLE and not self.protocol.isSaved():
-                        project.resetWorkFlow(workflowProtocolList)
-                    elif result == RESULT_CANCEL:
-                        return
-                elif not askYesNo(pwutils.Message.TITLE_RESTART_FORM,
-                                  pwutils.Message.LABEL_RESTART_FORM % (
-                                          '*%s*' % self.protocol.getRunName()),
-                                  self.root):
-                    return
-
-        errors = self.protocol.validate()
+
+        errors = []
+        resultAction = RESULT_RUN_SINGLE
+        mode = MODE_RESTART if self.protocol.getRunMode() == MODE_RESTART else MODE_RESUME
+
+        # we only take into account the protocols that are already part of the workflow
+        if not self.protocol.isNew():
+            from pyworkflow.gui.project.viewprotocols import ProtocolsView
+            errors, resultAction = ProtocolsView._launchSubWorkflow(self.protocol,
+                                                                    mode, self.root,
+                                                                    askSingleAll=True)
+        if resultAction == RESULT_CANCEL:
+            return
+        elif resultAction == RESULT_RUN_ALL:
+            self._close()
+            return
+
+        errors += self.protocol.validate()
 
         if errors:
             self.showInfo(errors)
@@ -2643,12 +2639,12 @@ def __init__(self, window, queueDict):
         Dialog.__init__(self, window.root, "Queue parameters")
 
     def body(self, bodyFrame):
-        bodyFrame.config(bg='white')
-        self.content = tk.Frame(bodyFrame, bg='white')
+        bodyFrame.config(bg=pw.Config.SCIPION_BG_COLOR)
+        self.content = tk.Frame(bodyFrame, bg=pw.Config.SCIPION_BG_COLOR)
         self.content.grid(row=0, column=0, padx=20, pady=20)
 
         label = tk.Label(self.content, text='Submit to queue',
-                         font=self.window.fontBold, bg='white')
+                         font=self.window.fontBold, bg=pw.Config.SCIPION_BG_COLOR)
         label.grid(row=0, column=0, sticky='ne', padx=5, pady=5)
         self.queueVar = tk.StringVar()
         self.queueVar.trace('w', self._onQueueChanged)
@@ -2689,7 +2685,7 @@ def _onQueueChanged(self, *args):
             else:
                 raise Exception('Incorrect number of params for %s, expected 3 or 4' % p[0])
 
-            label = tk.Label(self.content, text=label, bg='white')
+            label = tk.Label(self.content, text=label, bg=pw.Config.SCIPION_BG_COLOR)
             label.grid(row=r, column=0, sticky='ne', padx=5, pady=(0, 5))
             var = tk.StringVar()
             # Set the value coming in the protocol 
diff --git a/pyworkflow/gui/graph.py b/pyworkflow/gui/graph.py
index 8209577e4..6277f1e18 100644
--- a/pyworkflow/gui/graph.py
+++ b/pyworkflow/gui/graph.py
@@ -130,8 +130,6 @@ def _defaultCreateNode(self, canvas, node, y):
             raise Exception("method setCanvas should be called before using _defaultCreateNode")
         nodeText = node.getLabel()
         textColor = 'black'
-        if nodeText.startswith('Project'):
-            textColor = 'white'
 
         return canvas.createTextbox(nodeText, 100, y, bgColor='light blue', textColor=textColor, margin=0)
 
diff --git a/pyworkflow/gui/graph_layout.py b/pyworkflow/gui/graph_layout.py
index e6a616c0a..b686fd15b 100644
--- a/pyworkflow/gui/graph_layout.py
+++ b/pyworkflow/gui/graph_layout.py
@@ -62,70 +62,20 @@ def getFontScaleFactor(self):
 
         return self._fontScaleFactor
 
-
     def draw(self, graph, **kwargs):
         """ Setup the nodes position in the plane. """
         pass
 
 
-class BasicLayout(GraphLayout):
-    """ This layout will keep node position as much as possible.
-    It will try to allocate the nodes with x=0 and y=0.
-    """
-    def draw(self, graph, **kwargs):
-        """ Organize nodes of the graph in the plane.
-        Nodes should have: x, y, width and height attributes
-        x and y will be modified.
-        """
-        for node in graph.getNodes():
-            if hasattr(node, 'x') and hasattr(node, 'y'):
-                if getattr(node, 'x', 0) == 0 or node.y == 0:
-                    self._drawNode(node)
-                
-    def _drawNode(self, node):
-        """ Allocate node with x=0 and y=0. """
-
-        try:
-            parents = node.getParents()
-            if not parents:
-                logger.info("EMPTY NODE")
-                return
-            maxParent = parents[0]
-
-            for p in parents[1:]:
-                if p.y > maxParent.y:
-                    maxParent = p
-
-            siblings = maxParent.getChilds()
-
-            if len(siblings) == 1:
-                node.x = maxParent.x
-                node.y = maxParent.y + self.getY()
-            else:
-                rightSibling = siblings[0]
-                for s in siblings:
-                    if s.x > rightSibling.x:
-                        rightSibling = s
-                node.x = rightSibling.x + rightSibling.width/2 + self.DX + node.width/2
-                node.y = rightSibling.y
-        except Exception as e:
-            if Config.debugOn():
-                logger.debug("Can't draw node: %s" % node, e)
-                import traceback
-                logger.debug("".join(traceback.format_stack()))
-            else:
-                # Do nothing
-                return
-
-
 class LevelTreeLayout(GraphLayout):
     """ Organize the nodes of the graph by levels.
-    It will recursively organize childs and then
-    fit two sibling trees. """
+    It will recursively organize children and then
+    fit the sibling trees. """
     
-    def __init__(self):
+    def __init__(self, partial=False):
         GraphLayout.__init__(self)
         self.maxLevel = 9999
+        self.partial = partial
 
     def draw(self, graph, **kwargs):
         """
@@ -152,6 +102,9 @@ def draw(self, graph, **kwargs):
         # Clean temporary _layout attributes
         for node in graph.getNodes():
             del node._layout
+
+    def _isNewNode(self, node):
+        return node.x == 0 or node.y == 0 or node.isRoot()
         
     def _setLayoutLevel(self, node, level, parent):
         """ Iterate over all nodes and set _layout dict.
@@ -166,7 +119,8 @@ def _setLayoutLevel(self, node, level, parent):
         if level > layout.get('level', 0):
             # Calculate the y-position depending on the level
             # and the delta-Y (DY)
-            node.y = level * self.getY()
+            if not self.partial or self._isNewNode(node):
+                node.y = level * self.getY()
             layout['level'] = level
             layout['parent'] = parent
             if hasattr(node, 'width'):
@@ -302,7 +256,9 @@ def _applyNodeOffsets(self, node, x):
             return 
         
         layout = node._layout
-        node.x = x + layout['offset']
+
+        if not self.partial or self._isNewNode(node):
+            node.x = x + layout['offset']
         
         childs = self.__getNodeChilds(node)
         
diff --git a/pyworkflow/gui/gui.py b/pyworkflow/gui/gui.py
index f88e6452f..56096408e 100644
--- a/pyworkflow/gui/gui.py
+++ b/pyworkflow/gui/gui.py
@@ -28,11 +28,12 @@
 from functools import partial
 from tkinter.ttk import Style
 
-from pyworkflow.object import Object
 import pyworkflow as pw
-from pyworkflow.utils import Message, Color, Icon
-
+from pyworkflow.object import Object
+from pyworkflow.utils import Message, Icon
+from PIL import Image, ImageTk
 from .widgets import Button
+import numpy as np
 
 # --------------- GUI CONFIGURATION parameters -----------------------
 # TODO: read font size and name from config file
@@ -44,29 +45,29 @@
 cfgFontSize = pw.Config.SCIPION_FONT_SIZE
 cfgFontBigSize = cfgFontSize + 8
 # TextColor
-cfgCitationTextColor = "dark olive green"
-cfgLabelTextColor = "black"
-cfgSectionTextColor = "blue4"
+# cfgCitationTextColor = "dark olive green"
+# cfgLabelTextColor = "black"
+# cfgSectionTextColor = "blue4"
 # Background Color
-cfgBgColor = "light grey"
-cfgLabelBgColor = "white"
-cfgHighlightBgColor = cfgBgColor
-cfgButtonFgColor = "white"
-cfgButtonActiveFgColor = "white"
-cfgButtonBgColor = Color.RED_COLOR
-cfgButtonActiveBgColor = "#A60C0C"
+# cfgBgColor = "light grey"
+# cfgLabelBgColor = "white"
+# cfgHighlightBgColor = cfgBgColor
+cfgButtonFgColor = pw.Config.SCIPION_BG_COLOR
+cfgButtonActiveFgColor = pw.Config.SCIPION_BG_COLOR
+cfgButtonBgColor = pw.Config.SCIPION_MAIN_COLOR
+cfgButtonActiveBgColor = pw.Config.getActiveColor()
 cfgEntryBgColor = "lemon chiffon"
-cfgExpertLabelBgColor = "light salmon"
-cfgSectionBgColor = cfgButtonBgColor
+# cfgExpertLabelBgColor = "light salmon"
+# cfgSectionBgColor = cfgButtonBgColor
 # Color
-cfgListSelectColor = "DeepSkyBlue4"
-cfgBooleanSelectColor = "white"
-cfgButtonSelectColor = "DeepSkyBlue2"
+# cfgListSelectColor = "DeepSkyBlue4"
+# cfgBooleanSelectColor = "white"
+# cfgButtonSelectColor = "DeepSkyBlue2"
 # Dimensions limits
-cfgMaxHeight = 650
+# cfgMaxHeight = 650
 cfgMaxWidth = 800
-cfgMaxFontSize = 14
-cfgMinFontSize = 6
+# cfgMaxFontSize = 14
+# cfgMinFontSize = 6
 cfgWrapLenght = cfgMaxWidth - 50
 
 # Style of treeviews where row height is variable based on the font size
@@ -204,8 +205,9 @@ def getImage(imageName, imgDict=None, tkImage=True, percent=100,
         imagePath = imageName
     image = None
     if imagePath:
-        from PIL import Image
         image = Image.open(imagePath)
+        # For a future dark mode we might need to invert the image but it requires some extra work to make it look nice:
+        # image = invertImage(image)
         w, h = image.size
         newSize = None
         if percent != 100:  # Display image with other dimensions
@@ -216,12 +218,24 @@ def getImage(imageName, imgDict=None, tkImage=True, percent=100,
         if newSize:
             image.thumbnail(newSize, Image.ANTIALIAS)
         if tkImage:
-            from PIL import ImageTk
             image = ImageTk.PhotoImage(image)
+
         image_cache[imageName] = image
     return image
 
+def invertImage(img):
+    # Creating a numpy array out of the image object
+    img_arry = np.array(img)
+
+    # Maximum intensity value of the color mode
+    I_max = 255
+
+    # Subtracting 255 (max value possible in a given image
+    # channel) from each pixel values and storing the result
+    img_arry = I_max - img_arry
 
+    # Creating an image object from the resultant numpy array
+    return Image.fromarray(img_arry)
 # ---------------- Windows geometry utilities -----------------------
 def getGeometry(win):
     """ Return the geometry information of the windows
@@ -236,7 +250,7 @@ def centerWindows(root, dim=None, refWindows=None):
     or in the middle of other windows(refWindows param)"""
     root.update_idletasks()
     if dim is None:
-        gw, gh, gx, gy = getGeometry(root)
+        gw, gh, _, _ = getGeometry(root)
     else:
         gw, gh = dim
     if refWindows:
@@ -271,13 +285,15 @@ def defineStyle():
     defaultFont = getDefaultFont()
     rowheight = defaultFont.metrics()['linespace']
 
-    style.configure(LIST_TREEVIEW, rowheight=rowheight)
+    style.configure(LIST_TREEVIEW, rowheight=rowheight,
+                    background=pw.Config.SCIPION_BG_COLOR,
+                    fieldbackground=pw.Config.SCIPION_BG_COLOR)
     style.configure(LIST_TREEVIEW+".Heading", font=(defaultFont["family"],defaultFont["size"]))
 
 
 class Window:
     """Class to manage a Tk windows.
-    It will encapsulates some basic creation and 
+    It will encapsulate some basic creation and
     setup functions. """
     # To allow plugins to add their own menus
     _pluginMenus = dict()
@@ -306,9 +322,10 @@ def __init__(self, title='', masterWindow=None, weight=True,
             # he first window generated a tk.Toplevel. After that, all steps executed later will go through the else
             # statement, being that way each new tk.Toplevel() correctly referenced.
             tk.Tk().withdraw()  # Main window, invisible
-            self.root = tk.Toplevel()  # Toplevel of main window
+            self.root = tk.Toplevel(class_="Scipion Framework")  # Toplevel of main window
         else:
-            self.root = tk.Toplevel(masterWindow.root)
+            self.root = tk.Toplevel(masterWindow.root, class_="Scipion Framework")
+            self.root.group(masterWindow.root)
             self._images = masterWindow._images
 
         self.root.withdraw()
diff --git a/pyworkflow/gui/project/base.py b/pyworkflow/gui/project/base.py
index 3458ee9b1..db15607e0 100644
--- a/pyworkflow/gui/project/base.py
+++ b/pyworkflow/gui/project/base.py
@@ -65,7 +65,7 @@ def __init__(self, title, masterWindow=None, weight=True,
         self.header = self.createHeaderFrame(content)
         self.header.grid(row=0, column=0, sticky='new')
         
-        self.footer = tk.Frame(content, bg='white')
+        self.footer = tk.Frame(content, bg=pw.Config.SCIPION_BG_COLOR)
         self.footer.grid(row=1, column=0, sticky='news') 
         
         self.view, self.viewWidget = None, None
@@ -83,25 +83,25 @@ def createHeaderFrame(self, parent):
             - Project Name
             - View selection combobox
         """
-        header = tk.Frame(parent, bg='white')        
+        header = tk.Frame(parent, bg=pw.Config.SCIPION_BG_COLOR)
         header.columnconfigure(1, weight=1)
         header.columnconfigure(2, weight=1)
         # Create the SCIPION logo label
         logoImg = self.getImage(self.generalCfg.logo.get())
         logoLabel = tk.Label(header, image=logoImg, 
-                             borderwidth=0, anchor='nw', bg='white')
+                             borderwidth=0, anchor='nw', bg=pw.Config.SCIPION_BG_COLOR)
         logoLabel.grid(row=0, column=0, sticky='nw', padx=(5, 0), pady=5)
         version = "%s - %s (core)" % (os.environ.get('SCIPION_VERSION', ""), pw.LAST_VERSION)
 
         versionLabel = tk.Label(header, text=version,
-                                bg='white')
+                                bg=pw.Config.SCIPION_BG_COLOR)
         versionLabel.grid(row=0, column=1, sticky='sw', pady=20)
         
         # Create the Project Name label
         projName = getattr(self, 'projName', '')
         projLabel = tk.Label(header, text=projName, font=getBigFont(),
-                             borderwidth=0, anchor='nw', bg='white',
-                             fg=Color.DARK_GREY_COLOR)
+                             borderwidth=0, anchor='nw', bg=pw.Config.SCIPION_BG_COLOR,
+                             fg=Color.ALT_COLOR_DARK)
         projLabel.grid(row=0, column=2, sticky='sw', padx=(20, 5), pady=10)
         # Create gradient
         GradientFrame(header, height=8, borderwidth=0).grid(row=1, column=0,
@@ -113,17 +113,17 @@ def addViewList(self, header):
         """Create the view selection frame (Protocols|Data) in the header.
         """
         # This function is called from createHeaderFrame() in ProjectWindow
-        viewFrame = tk.Frame(header, bg='white')
+        viewFrame = tk.Frame(header, bg=pw.Config.SCIPION_BG_COLOR)
         viewFrame.grid(row=0, column=2, sticky='se', padx=5, pady=10)
 
         def addLink(elementText):
             btn = tk.Label(viewFrame, text=elementText, cursor='hand2',
-                           fg="#6F3232", bg="white")
+                           fg=pw.Config.SCIPION_MAIN_COLOR, bg=pw.Config.SCIPION_BG_COLOR)
             btn.bind('<Button-1>', lambda e: self._viewComboSelected(elementText))
             return btn
         
         def addTube():        
-            return tk.Label(viewFrame, text="|", fg="#6F3232", bg="white", padx=5)
+            return tk.Label(viewFrame, text="|", fg=pw.Config.SCIPION_MAIN_COLOR, bg=pw.Config.SCIPION_BG_COLOR, padx=5)
 
         for i, elementText in enumerate(VIEW_LIST):
             btn = addLink(elementText)
diff --git a/pyworkflow/gui/project/constants.py b/pyworkflow/gui/project/constants.py
index 04e0ec751..22325cf5d 100644
--- a/pyworkflow/gui/project/constants.py
+++ b/pyworkflow/gui/project/constants.py
@@ -30,8 +30,7 @@
                                  STATUS_INTERACTIVE, STATUS_ABORTED,
                                  STATUS_SCHEDULED)
 
-from pyworkflow.utils.properties import Message, Icon
-
+from pyworkflow.utils.properties import Message, Icon, KEYSYM
 
 STATUS_COLORS = {
                STATUS_SAVED: '#D9F1FA',
@@ -53,6 +52,8 @@
 ACTION_SELECT_FROM = Message.LABEL_SELECT_FROM
 ACTION_SELECT_TO = Message.LABEL_SELECT_TO
 ACTION_COPY = Message.LABEL_COPY
+ACTION_PASTE = Message.LABEL_PASTE
+ACTION_DUPLICATE = Message.LABEL_DUPLICATE
 ACTION_DELETE = Message.LABEL_DELETE
 ACTION_REFRESH = Message.LABEL_REFRESH
 ACTION_STEPS = Message.LABEL_STEPS
@@ -79,11 +80,13 @@
     ACTION_EDIT: Icon.ACTION_EDIT,
     ACTION_SELECT_FROM: Icon.ACTION_SELECT_FROM,
     ACTION_SELECT_TO: Icon.ACTION_SELECT_TO,
+    ACTION_DUPLICATE: Icon.ACTION_DUPLICATE,
     ACTION_COPY: Icon.ACTION_COPY,
+    ACTION_PASTE:Icon.ACTION_PASTE,
     ACTION_DELETE: Icon.ACTION_DELETE,
     ACTION_REFRESH: Icon.ACTION_REFRESH,
     ACTION_RENAME: Icon.ACTION_RENAME,
-    ACTION_STEPS: Icon.ACTION_STEPS,
+    ACTION_STEPS: Icon.CODE_BRANCH,
     ACTION_BROWSE: Icon.ACTION_BROWSE,
     ACTION_DB: Icon.ACTION_DB,
     ACTION_TREE: None,  # should be set
@@ -95,9 +98,40 @@
     ACTION_COLLAPSE: Icon.ACTION_COLLAPSE,
     ACTION_EXPAND: Icon.ACTION_EXPAND,
     ACTION_LABELS: Icon.TAGS,
-    ACTION_RESTART_WORKFLOW: Icon.ACTION_EXECUTE,
+    ACTION_RESTART_WORKFLOW: Icon.BACKWARD,
     ACTION_CONTINUE_WORKFLOW: Icon.ACTION_CONTINUE,
     ACTION_STOP_WORKFLOW: Icon.ACTION_STOP_WORKFLOW,
-    ACTION_RESET_WORKFLOW: Icon.ACTION_REFRESH,
-    ACTION_SEARCH: Icon.ACTION_SEARCH
+    ACTION_RESET_WORKFLOW: Icon.BROOM,
+    ACTION_SEARCH: Icon.ACTION_SEARCH,
+}
+
+# TODO: Centralize this and the 2 above collections
+ActionShortCuts = {
+    # F keys
+    ACTION_RENAME: "<F2>",
+    ACTION_REFRESH: "<F5>",
+    # Letter keys, uppercase first, sorted
+    ACTION_COLLAPSE: "-",
+    ACTION_EXPAND: "+",
+    ACTION_RESULTS: "a",
+    ACTION_BROWSE: "b",
+    ACTION_CONTINUE_WORKFLOW:"c",
+    ACTION_DB: "d",
+    ACTION_EDIT: "e",
+    ACTION_SELECT_FROM: "f",
+    ACTION_LABELS: "l",
+    ACTION_TREE: "o",
+    ACTION_RESTART_WORKFLOW: "R",
+    ACTION_RESET_WORKFLOW:"r",
+    ACTION_STEPS: "S",
+    ACTION_STOP: "s",
+    ACTION_SELECT_TO: "t",
+    # Control +, sorted
+    ACTION_COPY: "<Control-c>",
+    ACTION_DUPLICATE: "<Control-d>",
+    ACTION_PASTE: "<Control-v>",
+    ACTION_STOP_WORKFLOW: "<Control-s>",
+    ACTION_DELETE: "<Delete>",
+
+
 }
diff --git a/pyworkflow/gui/project/labels.py b/pyworkflow/gui/project/labels.py
index 95a0dbf31..b7f6a40e8 100644
--- a/pyworkflow/gui/project/labels.py
+++ b/pyworkflow/gui/project/labels.py
@@ -28,7 +28,7 @@
 """
         
 import tkinter as tk
-
+from pyworkflow import Config
 from pyworkflow.project import Label
 from pyworkflow.gui import Icon, configureWeigths
 from pyworkflow.gui.tree import TreeProvider
@@ -148,11 +148,11 @@ def __init__(self, parent, title, label, **kwargs):
         dialog.Dialog.__init__(self, parent, title)
 
     def body(self, bodyFrame):
-        bodyFrame.config(bg='white')
+        bodyFrame.config(bg=Config.SCIPION_BG_COLOR)
         configureWeigths(bodyFrame, 1, 1)
 
         # Label
-        label_text = tk.Label(bodyFrame, text="Name", bg='white', bd=0)
+        label_text = tk.Label(bodyFrame, text="Name", bg=Config.SCIPION_BG_COLOR, bd=0)
         label_text.grid(row=0, column=0, sticky='nw', padx=(15, 10), pady=15)
         # Label box
         var = tk.StringVar()
@@ -163,7 +163,7 @@ def body(self, bodyFrame):
 
         # Comment
         colorLabel = tk.Label(bodyFrame, text='Color \n(Click to change)',
-                              bg='white', bd=0)
+                              bg=Config.SCIPION_BG_COLOR, bd=0)
         colorLabel.grid(row=1, column=0, sticky='nw', padx=(15, 10), pady=15)
         self.colorVar = tk.StringVar()
         self.colorVar.set(self.label.getColor())
diff --git a/pyworkflow/gui/project/project.py b/pyworkflow/gui/project/project.py
index 4edf122db..72b1e37ea 100644
--- a/pyworkflow/gui/project/project.py
+++ b/pyworkflow/gui/project/project.py
@@ -89,14 +89,10 @@ 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('Find protocol to add', 'find protocol',
+        projMenu.addSubMenu('Add a protocol', 'find protocol',
                             shortCut="Ctrl+f", icon=Icon.FIND)
-        projMenu.addSubMenu('Scipion log', 'scipion log',
-                            icon=Icon.FILE_BW)
         projMenu.addSubMenu('Locate a protocol', 'locate protocol',
                             shortCut="Ctrl+l")
-        projMenu.addSubMenu('Add a protocol', 'find protocol',
-                            shortCut="Ctrl+f")
         projMenu.addSubMenu('', '')  # add separator
         projMenu.addSubMenu('Import workflow', 'load_workflow',
                             icon=Icon.DOWNLOAD)
@@ -106,9 +102,11 @@ def __init__(self, path, master=None):
             projMenu.addSubMenu('Export tree graph', 'export_tree')
         projMenu.addSubMenu('', '')  # add separator
         projMenu.addSubMenu('Debug Mode', 'debug mode',
-                            shortCut="Ctrl+d", icon=Icon.DEBUG)
+                            shortCut="Ctrl+D", icon=Icon.DEBUG)
         projMenu.addSubMenu('', '')  # add separator
         projMenu.addSubMenu('Notes', 'notes', icon=Icon.ACTION_EDIT)
+        projMenu.addSubMenu('Scipion log', 'scipion log',
+                            icon=Icon.FILE_BW)
         projMenu.addSubMenu('', '')  # add separator
         projMenu.addSubMenu('Exit', 'exit', icon=Icon.ACTION_OUT)
 
@@ -236,9 +234,9 @@ def onRemoveTemporaryFiles(self):
         
     def _loadWorkflow(self, obj):
         try:
-            self.getViewWidget().info('Importing the workflow...')
+            self.getViewWidget().info('Importing workflow %s' % obj.getPath())
             self.project.loadProtocols(obj.getPath())
-            self.getViewWidget().updateRunsGraph(True, reorganize=False)
+            self.getViewWidget().updateRunsGraph(True)
             self.getViewWidget().cleanInfo()
         except Exception as ex:
             self.showError(str(ex), exception=ex)
diff --git a/pyworkflow/gui/project/searchprotocol.py b/pyworkflow/gui/project/searchprotocol.py
index 0a5e28356..d88fc47ec 100644
--- a/pyworkflow/gui/project/searchprotocol.py
+++ b/pyworkflow/gui/project/searchprotocol.py
@@ -103,7 +103,7 @@ def scoreProtocols(self):
                         prot.getHelpText().strip().replace('\r', '').replace('\n', '').lower(),
                         "streamified" if prot.worksInStreaming() else "static",
                         BETA if prot.isBeta() else "",
-                        NEW if prot.isNew() else "",
+                        NEW if prot.isNewDev() else "",
                         UPDATED if prot.isUpdated() else "")
 
                 line = self._addSearchWeight(line, keyword)
diff --git a/pyworkflow/gui/project/searchrun.py b/pyworkflow/gui/project/searchrun.py
index 64c78cab0..20af250f7 100644
--- a/pyworkflow/gui/project/searchrun.py
+++ b/pyworkflow/gui/project/searchrun.py
@@ -29,11 +29,7 @@
 import pyworkflow.protocol as pwprot
 
 from pyworkflow.gui import SearchBaseWindow
-from pyworkflow.gui.project.constants import ACTION_COPY, ACTION_EDIT, ACTION_RENAME, ACTION_DELETE, ACTION_STEPS, \
-    ACTION_BROWSE, ACTION_DB, ACTION_STOP, ACTION_EXPORT, ACTION_EXPORT_UPLOAD, ACTION_COLLAPSE, ACTION_EXPAND, \
-    ACTION_LABELS, ACTION_SELECT_FROM, ACTION_SELECT_TO, ACTION_RESTART_WORKFLOW, ACTION_CONTINUE_WORKFLOW, \
-    ACTION_STOP_WORKFLOW, ACTION_RESET_WORKFLOW, ActionIcons
-
+from pyworkflow.gui.project.constants import *
 from pyworkflow.gui.tree import ProjectRunsTreeProvider
 
 class RunsTreeProvider(ProjectRunsTreeProvider):
@@ -62,7 +58,9 @@ def getActionsFromSelection(self):
 
         return [(ACTION_EDIT, single and status and expanded),
                 (ACTION_RENAME, single and status and expanded),
+                (ACTION_DUPLICATE, status and expanded),
                 (ACTION_COPY, status and expanded),
+                (ACTION_PASTE, status and expanded),
                 (ACTION_DELETE, status != pwprot.STATUS_RUNNING and status and expanded),
                 (ACTION_STEPS, single and Config.debugOn() and status and expanded),
                 (ACTION_BROWSE, single and status and expanded),
@@ -88,7 +86,8 @@ def addAction(actionLabel):
                 text = actionLabel
                 action = actionLabel
                 actionLabel = (text, lambda: self.actionFunc(action),
-                               ActionIcons.get(action, None))
+                               ActionIcons.get(action, None),
+                               ActionShortCuts.get(action,None))
             return actionLabel
 
         actions = [addAction(a)
diff --git a/pyworkflow/gui/project/steps.py b/pyworkflow/gui/project/steps.py
index e05cf96fa..517d7a3e4 100644
--- a/pyworkflow/gui/project/steps.py
+++ b/pyworkflow/gui/project/steps.py
@@ -95,14 +95,14 @@ def fillToolBar(self):
 
         # Tree button
         btn = tk.Label(toolbar, text="Tree",
-                       image=self.getImage(Icon.RUNS_TREE),
+                       image=self.getImage(Icon.CODE_BRANCH),
                        compound=tk.LEFT, cursor='hand2')
         btn.bind(TK.LEFT_CLICK, self._showTree)
         btn.grid(row=0, column=0, sticky='nw')
 
         # Reset status
         btn = tk.Label(toolbar, text="Reset",
-                       image=self.getImage(Icon.ACTION_REFRESH),
+                       image=self.getImage(Icon.BROOM),
                        compound=tk.LEFT, cursor='hand2')
         btn.bind('<Button-1>', self._resetStep)
         btn.grid(row=0, column=1, sticky='nw')
@@ -145,7 +145,7 @@ def _stepTooltip(self, tw, item):
             frame = tk.Frame(tw)
             frame.grid(row=0, column=0)
             tw.tooltipText = pwgui.dialog.createMessageBody(
-                frame, tm, None, textPad=0, textBg=pwutils.Color.LIGHT_GREY_COLOR_2)
+                frame, tm, None, textPad=0, textBg=pwutils.Color.ALT_COLOR_2)
             tw.tooltipText.config(bd=1, relief=tk.RAISED)
         else:
             pwgui.dialog.fillMessageText(tw.tooltipText, tm)
diff --git a/pyworkflow/gui/project/viewdata.py b/pyworkflow/gui/project/viewdata.py
index 0bb2beaed..4644c2cf5 100644
--- a/pyworkflow/gui/project/viewdata.py
+++ b/pyworkflow/gui/project/viewdata.py
@@ -31,6 +31,7 @@
 import tkinter.ttk as ttk
 import tkinter.font as tkFont
 
+from pyworkflow import Config
 import pyworkflow.utils as pwutils
 import pyworkflow.protocol as pwprot
 import pyworkflow.viewer as pwviewer
@@ -122,16 +123,16 @@ def _createContent(self):
             Left: containing the Protocol classes tree
             Right: containing the Data list
         """
-        p = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg='white')
+        p = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg=Config.SCIPION_BG_COLOR)
         
         # Left pane, contains Data tree
-        leftFrame = tk.Frame(p, bg='white')
+        leftFrame = tk.Frame(p, bg=Config.SCIPION_BG_COLOR)
         bgColor = '#eaebec'
         self._createDataTree(leftFrame, bgColor)
         gui.configureWeigths(leftFrame)
         
         # Right pane
-        rightFrame = tk.Frame(p, bg='white')
+        rightFrame = tk.Frame(p, bg=Config.SCIPION_BG_COLOR)
         rightFrame.columnconfigure(0, weight=1)
         rightFrame.rowconfigure(1, weight=1)
         # rightFrame.rowconfigure(0, minsize=label.winfo_reqheight())
@@ -151,7 +152,7 @@ def _createDataTree(self, parent, bgColor):
         """
         defaultFont = gui.getDefaultFont()
         self.style.configure("W.Treeview",
-                             background=pwutils.Color.LIGHT_GREY_COLOR,
+                             background=pwutils.Color.ALT_COLOR,
                              borderwidth=0,
                              rowheight=defaultFont.metrics()['linespace'])
         self.dataTree = Tree(parent, show='tree', style='W.Treeview')
@@ -215,31 +216,31 @@ def _fillRightPane(self, parent):
         # Selected run info (at Bottom)
         """
         # Create the Action Buttons TOOLBAR
-        toolbar = tk.Frame(parent, bg='white')
+        toolbar = tk.Frame(parent, bg=Config.SCIPION_BG_COLOR)
         toolbar.grid(row=0, column=0, sticky='news')
         gui.configureWeigths(toolbar)
         # toolbar.columnconfigure(0, weight=1)
         toolbar.columnconfigure(1, weight=1)
         
-        self.runsToolbar = tk.Frame(toolbar, bg='white')
+        self.runsToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
         self.runsToolbar.grid(row=0, column=0, sticky='sw')
         # On the left of the toolbar will be other
         # actions that can be applied to all runs (refresh, graph view...)
-        self.allToolbar = tk.Frame(toolbar, bg='white')
+        self.allToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
         self.allToolbar.grid(row=0, column=10, sticky='se')
         # self.createActionToolbar()
 
         # Create the Run History tree
         v = ttk.PanedWindow(parent, orient=tk.VERTICAL)
         # runsFrame = ttk.Labelframe(v, text=' History ', width=500, height=500)
-        runsFrame = tk.Frame(v, bg='white')
+        runsFrame = tk.Frame(v, bg=Config.SCIPION_BG_COLOR)
         self._createDataGraph(runsFrame)
         gui.configureWeigths(runsFrame)
         
         # Create the Selected Run Info
         infoFrame = tk.Frame(v)
         gui.configureWeigths(infoFrame)
-        self._infoText = TaggedText(infoFrame, bg='white',
+        self._infoText = TaggedText(infoFrame, bg=Config.SCIPION_BG_COLOR,
                                     handlers={'sci-open': self._openProtocolFormFromId})
         self._infoText.grid(row=0, column=0, sticky='news')
         
diff --git a/pyworkflow/gui/project/viewprojects.py b/pyworkflow/gui/project/viewprojects.py
index 1f3ecfa82..fcf7f3c15 100644
--- a/pyworkflow/gui/project/viewprojects.py
+++ b/pyworkflow/gui/project/viewprojects.py
@@ -21,7 +21,10 @@
 # *  e-mail address 'scipion@cnb.csic.es'
 # *
 # **************************************************************************
+import datetime
 import logging
+
+REFRESH_WAIT_SEC = 60
 logger = logging.getLogger(__name__)
 
 import os
@@ -38,7 +41,7 @@
 from pyworkflow.gui.text import TaggedText
 from pyworkflow.gui.dialog import askString, askYesNo, showError
 
-from pyworkflow.gui import Message, Window, cfgEntryBgColor
+from pyworkflow.gui import Message, Window, cfgEntryBgColor, ToolTip
 from pyworkflow.gui.browser import FileBrowserWindow
 from pyworkflow.gui.widgets import IconButton, HotButton, Button
 from pyworkflow.utils.properties import Icon
@@ -48,12 +51,14 @@ class ProjectsView(tk.Frame):
     _PROJ_CONTAINER = "projectsframe"
 
     def __init__(self, parent, windows, **args):
-        tk.Frame.__init__(self, parent, bg='white', **args)
+        tk.Frame.__init__(self, parent, bg=pw.Config.SCIPION_BG_COLOR, **args)
         self.windows = windows
         self.manager = windows.manager
         self.root = windows.root
+        self.lastLoad = None
 
-        # tkFont.Font(size=12, family='verdana', weight='bold')
+        # Bind to root "focus in"
+        self.root.bind("<FocusIn>", self._onWindowFocusIn)
         bigSize = pwgui.cfgFontSize + 2
         smallSize = pwgui.cfgFontSize - 2
         fontName = pwgui.cfgFontName
@@ -71,19 +76,27 @@ def __init__(self, parent, windows, **args):
 
         self.columnconfigure(0, weight=1)
         self.rowconfigure(1, weight=1)
-        text = TaggedText(self, width=40, height=15, bd=0, bg='white')
+        text = TaggedText(self, width=40, height=15, bd=0, bg=pw.Config.SCIPION_BG_COLOR)
         text.grid(row=1, columnspan=2, column=0, sticky='news')
-
-        self.createProjectList(text)
         text.setReadOnly(True)
         self.text = text
         self.filterBox.focus_set()
 
+        # Content load happens automatically _onWindowFocusIn
+
+    def _onWindowFocusIn(self, event):
+        """ Refresh on windows get focus """
+        if event.widget == self.root:
+            # Get the delta from the last time refreshed
+            delta = REFRESH_WAIT_SEC if self.lastLoad is None else (datetime.datetime.now() - self.lastLoad).seconds
+            if delta >= REFRESH_WAIT_SEC:
+                self.createProjectList()
+
     def addActionsFrame(self):
         """ Add the "toolbar" for actions like create project, import
          project or filter"""
         # Add the create project button
-        bg = "white"
+        bg = pw.Config.SCIPION_BG_COLOR
         btnFrame = tk.Frame(self, bg=bg)
         btn = HotButton(btnFrame, text=Message.LABEL_CREATE_PROJECT,
                         font=self.projNameFont,
@@ -104,15 +117,21 @@ def addActionsFrame(self):
         self.filterBox.grid(row=0, column=3, sticky='ne', padx=10, pady=12)
         self.filterBox.bind('<Return>', self._onFilter)
         self.filterBox.bind('<KP_Enter>', self._onFilter)
+        self.filterBox.bind("<F5>", self.createProjectList)
+        ToolTip(self.filterBox, "Return/enter to filter. F5 to refresh the list")
+
 
-    def createProjectList(self, text):
+    def createProjectList(self, event=None):
         """Load the list of projects"""
+
+        self.lastLoad = datetime.datetime.now()
         r = 0
+        text = self.text
         text.setReadOnly(False)
         text.clear()
-        parent = tk.Frame(text, bg='white', name=self._PROJ_CONTAINER)
+        parent = tk.Frame(text, bg=pw.Config.SCIPION_BG_COLOR, name=self._PROJ_CONTAINER)
         parent.columnconfigure(0, weight=1)
-        colors = ['white', '#EAEBFF']
+        colors = [pw.Config.SCIPION_BG_COLOR, '#EAEBFF']
         for i, p in enumerate(self.manager.listProjects()):
             try:
                 # Add creation time to project info
@@ -168,7 +187,7 @@ def createProjectLabel(self, parent, projInfo, color):
 
     def createNewProject(self, projName, projLocation):
         proj = self.manager.createProject(projName, location=projLocation)
-        self.createProjectList(self.text)
+        self.createProjectList()
         self.openProject(proj.getShortName())
 
     def _onCreateProject(self, e=None):
@@ -180,7 +199,7 @@ def _onImportProject(self, e=None):
         importProjWindow.show()
 
     def _onFilter(self, e=None):
-        self.createProjectList(self.text)
+        self.createProjectList()
 
     def _setFocusToList(self, e=None):
         self.text.focus_set()
@@ -197,7 +216,7 @@ def _doesProjectMatchFilter(self, project):
     def importProject(self, projLocation, copyFiles, projName, searchLocation):
 
         self.manager.importProject(projLocation, copyFiles, projName, searchLocation)
-        self.createProjectList(self.text)
+        self.createProjectList()
         self.openProject(projName)
 
     def openProject(self, projName):
@@ -228,7 +247,7 @@ def renameProject(self, projName):
                       "Project name already exists: %s" % newName, self.root)
             return
         self.manager.renameProject(projName, newName)
-        self.createProjectList(self.text)
+        self.createProjectList()
 
 
 class ProjectCreateWindow(Window):
@@ -241,7 +260,7 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 110),
         """
         Window.__init__(self, title, parent.windows, weight=weight,
                         icon=icon, minsize=minsize, enableQueue=True)
-        self.root['background'] = 'white'
+        self.root['background'] = pw.Config.SCIPION_BG_COLOR
 
         self.parent = parent
         self.projectsPath = self.parent.manager.PROJECTS
@@ -253,21 +272,21 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 110),
         content = tk.Frame(self.root)
         content.columnconfigure(0, weight=1)
         content.columnconfigure(1, weight=3)
-        content.config(bg='white')
+        content.config(bg=pw.Config.SCIPION_BG_COLOR)
         content.grid(row=0, column=0, sticky='news', padx=5, pady=5)
 
         # Info line
-        labelInfo = tk.Label(content, text="Spaces will be replaced by underscores!", bg='white', bd=0)
+        labelInfo = tk.Label(content, text="Spaces will be replaced by underscores!", bg=pw.Config.SCIPION_BG_COLOR, bd=0)
         labelInfo.grid(row=0, sticky=tk.W, padx=5, pady=5)
         #  Project name line
-        labelName = tk.Label(content, text=Message.LABEL_PROJECT + ' name', bg='white', bd=0)
+        labelName = tk.Label(content, text=Message.LABEL_PROJECT + ' name', bg=pw.Config.SCIPION_BG_COLOR, bd=0)
         labelName.grid(row=1, sticky=tk.W, padx=5, pady=5)
         entryName = tk.Entry(content, bg=cfgEntryBgColor, width=20, textvariable=self.projName)
         entryName.grid(row=1, column=1, columnspan=2, sticky=tk.EW, padx=5, pady=5)
         entryName.bind("<Return>", self._create)
         entryName.bind("<KP_Enter>", self._create)
         # Project location line
-        labelLocation = tk.Label(content, text=Message.LABEL_PROJECT + ' location', bg='white', bd=0)
+        labelLocation = tk.Label(content, text=Message.LABEL_PROJECT + ' location', bg=pw.Config.SCIPION_BG_COLOR, bd=0)
         labelLocation.grid(row=2, column=0, sticky='nw', padx=5, pady=5)
 
         self.entryBrowse = tk.Entry(content, bg=cfgEntryBgColor, width=40, textvariable=self.projLocation)
@@ -282,7 +301,7 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 110),
         btnFrame = tk.Frame(content)
         btnFrame.columnconfigure(0, weight=1)
         btnFrame.grid(row=3, column=0, sticky='sew', padx=5, pady=(0, 5), columnspan=2)
-        btnFrame.config(bg='white')
+        btnFrame.config(bg=pw.Config.SCIPION_BG_COLOR)
 
         # Create buttons
         btnCreate = HotButton(btnFrame, 'Create', Icon.BUTTON_SELECT, command=self._create)
@@ -340,7 +359,7 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 150),
         """
         Window.__init__(self, title, parent.windows, weight=weight,
                         icon=icon, minsize=minsize, enableQueue=True)
-        self.root['background'] = 'white'
+        self.root['background'] = pw.Config.SCIPION_BG_COLOR
         self.parent = parent
         # Dirty hack, need to add a slash for the explorer to pick up the right default path.
         self.projectsPath = getHomePath() + "/"
@@ -356,12 +375,12 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 150),
         content = tk.Frame(self.root)
         content.columnconfigure(0, weight=1)
         content.columnconfigure(1, weight=1)
-        content.config(bg='white')
+        content.config(bg=pw.Config.SCIPION_BG_COLOR)
         content.grid(row=0, column=0, sticky='news',
                      padx=5, pady=5)
 
         # Path explorer
-        labelProjectLocation = tk.Label(content, text="Project location", bg='white', bd=0)
+        labelProjectLocation = tk.Label(content, text="Project location", bg=pw.Config.SCIPION_BG_COLOR, bd=0)
         labelProjectLocation.grid(row=0, column=0, sticky='nw', padx=5, pady=5)
         # it seems tk.Entry does not uses default font...grrrr!!
         self.entryBrowse = tk.Entry(content, bg=cfgEntryBgColor, width=40,
@@ -372,12 +391,12 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 150),
         self.btnBrowse.grid(row=0, column=2, sticky='e', padx=5, pady=5)
 
         # Copy files check
-        labelCheck = tk.Label(content, text="Copy project", bg='white', borderwidth=0)
+        labelCheck = tk.Label(content, text="Copy project", bg=pw.Config.SCIPION_BG_COLOR, borderwidth=0)
         labelCheck.grid(row=1, column=0, sticky='nw', padx=5, pady=5)
 
         self.tkCheckVar = tk.IntVar()
-        btnCheck = tk.Checkbutton(content, variable=self.tkCheckVar, highlightthickness=0, activebackground='white',
-                                  bg='white', bd=0)
+        btnCheck = tk.Checkbutton(content, variable=self.tkCheckVar, highlightthickness=0, activebackground=pw.Config.SCIPION_BG_COLOR,
+                                  bg=pw.Config.SCIPION_BG_COLOR, bd=0)
         btnCheck.grid(row=1, column=1, sticky='nw', padx=0, pady=5)
 
         btnCopyHelp = IconButton(content, Message.LABEL_BUTTON_HELP, Icon.ACTION_HELP, highlightthickness=0,
@@ -386,13 +405,13 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 150),
         btnCopyHelp.grid(row=1, column=3, sticky='e', padx=2, pady=2)
 
         # Project name
-        labelName = tk.Label(content, text='Project name (Optional)', bg='white', bd=0)
+        labelName = tk.Label(content, text='Project name (Optional)', bg=pw.Config.SCIPION_BG_COLOR, bd=0)
         labelName.grid(row=2, column=0, sticky='nw', padx=5, pady=5)
         entryName = tk.Entry(content, bg='white', width=20, textvariable=self.projName, font=self.font)
         entryName.grid(row=2, column=1, sticky='nw', padx=5, pady=5)
 
         # Path to search for raw data and restore broken links.
-        labelSearchLocation = tk.Label(content, text="Raw files location (Optional)", bg='white', bd=0)
+        labelSearchLocation = tk.Label(content, text="Raw files location (Optional)", bg=pw.Config.SCIPION_BG_COLOR, bd=0)
         labelSearchLocation.grid(row=3, column=0, sticky='nw', padx=5, pady=5)
 
         self.entrySearchLocation = tk.Entry(content, bg='white', width=40,
@@ -413,7 +432,7 @@ def __init__(self, title, parent=None, weight=True, minsize=(400, 150),
         btnFrame = tk.Frame(content)
         btnFrame.columnconfigure(0, weight=1)
         btnFrame.grid(row=4, column=0, sticky='sew', padx=5, pady=(0, 5), columnspan=2)
-        btnFrame.config(bg='white')
+        btnFrame.config(bg=pw.Config.SCIPION_BG_COLOR)
 
         # Create buttons
         btnSelect = HotButton(btnFrame, 'Import', Icon.BUTTON_SELECT, command=self._select)
diff --git a/pyworkflow/gui/project/viewprotocols.py b/pyworkflow/gui/project/viewprotocols.py
index 41e63a793..7b525af8d 100644
--- a/pyworkflow/gui/project/viewprotocols.py
+++ b/pyworkflow/gui/project/viewprotocols.py
@@ -24,13 +24,11 @@
 # **************************************************************************
 import logging
 import threading
+
 from pyworkflow import Config
-from pyworkflow.gui import TextFileViewer, getDefaultFont, LIST_TREEVIEW, ShortCut
-from pyworkflow.gui.project.constants import ACTION_REFRESH, ACTION_EDIT, ACTION_COPY, ACTION_DELETE, ACTION_STEPS, \
-    ACTION_BROWSE, ACTION_DB, ACTION_STOP, ACTION_CONTINUE, ACTION_RESULTS, ACTION_EXPORT, ACTION_EXPORT_UPLOAD, \
-    ACTION_COLLAPSE, ACTION_EXPAND, ACTION_LABELS, ACTION_SEARCH, ActionIcons, ACTION_TREE, ACTION_SWITCH_VIEW, \
-    ACTION_SELECT_TO, ACTION_RENAME, ACTION_RESTART_WORKFLOW, ACTION_CONTINUE_WORKFLOW, ACTION_DEFAULT, \
-    ACTION_SELECT_FROM, ACTION_STOP_WORKFLOW, ACTION_RESET_WORKFLOW
+from pyworkflow.gui import TextFileViewer, getDefaultFont, LIST_TREEVIEW, \
+    ShortCut, ToolTip, RESULT_RUN_ALL, RESULT_RUN_SINGLE, RESULT_CANCEL
+from pyworkflow.gui.project.constants import *
 from pyworkflow.protocol import SIZE_1MB, SIZE_1GB, SIZE_1TB
 
 INIT_REFRESH_SECONDS = Config.SCIPION_GUI_REFRESH_INITIAL_WAIT
@@ -165,24 +163,15 @@ def __init__(self, parent, windows, **args):
         self._lastRightClickPos = None  # Keep last right-clicked position
 
         self.style = ttk.Style()
-        self.root.bind("<F5>", self.refreshRuns)
         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)
+        self.root.bind("<Control-D>", self._toggleDebug)
         self.root.bind("<Control-l>", self._locateProtocol)
-        self.root.bind("<F2>", self._F2Pressed)
 
         if Config.debugOn():
             self.root.bind("<Control-i>", self._inspectProtocols)
 
-        # To bind key press to methods
-        # Listen to any key: send event to keyPressed method
-        self.root.bind("<Key>", self.keyPressed)
-        self.keybinds = dict()
-
-        # Register key binds
-        self._bindKeyPress(KEYSYM.DELETE, self._onDelPressed)
 
         self.__autoRefresh = None
         self.__autoRefreshCounter = INIT_REFRESH_SECONDS  # start by 3 secs
@@ -194,16 +183,6 @@ def __init__(self, parent, windows, **args):
         pwgui.configureWeigths(self)
         c.grid(row=0, column=0, sticky='news')
 
-    def _bindKeyPress(self, key, method):
-
-        self.keybinds[key] = method
-
-    def keyPressed(self, event):
-
-        if event.keysym in self.keybinds:
-            method = self.keybinds[event.keysym]
-
-            method()
 
     def createContent(self):
         """ Create the Protocols View for the Project.
@@ -211,8 +190,8 @@ def createContent(self):
             Left: containing the Protocol classes tree
             Right: containing the Runs list
         """
-        p = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg='white')
-        bgColor = Color.LIGHT_GREY_COLOR
+        p = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg=Config.SCIPION_BG_COLOR)
+        bgColor = Color.ALT_COLOR
         # Left pane, contains Protocols Pane
         leftFrame = tk.Frame(p, bg=bgColor)
         leftFrame.columnconfigure(0, weight=1)
@@ -231,30 +210,30 @@ def createContent(self):
         # Runs History (at Top)
 
         # Selected run info (at Bottom)
-        rightFrame = tk.Frame(p, bg='white')
+        rightFrame = tk.Frame(p, bg=Config.SCIPION_BG_COLOR)
         rightFrame.columnconfigure(0, weight=1)
         rightFrame.rowconfigure(1, weight=1)
         # rightFrame.rowconfigure(0, minsize=label.winfo_reqheight())
 
         # Create the Action Buttons TOOLBAR
-        toolbar = tk.Frame(rightFrame, bg='white')
+        toolbar = tk.Frame(rightFrame, bg=Config.SCIPION_BG_COLOR)
         toolbar.grid(row=0, column=0, sticky='news')
         pwgui.configureWeigths(toolbar)
         # toolbar.columnconfigure(0, weight=1)
         toolbar.columnconfigure(1, weight=1)
 
-        self.runsToolbar = tk.Frame(toolbar, bg='white')
+        self.runsToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
         self.runsToolbar.grid(row=0, column=0, sticky='sw')
         # On the left of the toolbar will be other
         # actions that can be applied to all runs (refresh, graph view...)
-        self.allToolbar = tk.Frame(toolbar, bg='white')
+        self.allToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
         self.allToolbar.grid(row=0, column=10, sticky='se')
         self.createActionToolbar()
 
         # Create the Run History tree
         v = ttk.PanedWindow(rightFrame, orient=tk.VERTICAL)
         # runsFrame = ttk.Labelframe(v, text=' History ', width=500, height=500)
-        runsFrame = tk.Frame(v, bg='white')
+        runsFrame = tk.Frame(v, bg=Config.SCIPION_BG_COLOR)
         # runsFrame.grid(row=1, column=0, sticky='news', pady=5)
         self.runsTree = self.createRunsTree(runsFrame)
         pwgui.configureWeigths(runsFrame)
@@ -277,18 +256,18 @@ def createContent(self):
         self.infoLabel.grid(row=0, column=0, sticky='w', padx=3)
         # Create the Analyze results button
         self.btnAnalyze = pwgui.Button(infoFrame, text=Message.LABEL_ANALYZE,
-                                       fg='white', bg=Color.RED_COLOR,
+                                       fg='white', bg=Config.SCIPION_MAIN_COLOR,
                                        image=self.getImage(Icon.ACTION_VISUALIZE),
                                        compound=tk.LEFT,
                                        activeforeground='white',
-                                       activebackground='#A60C0C',
+                                       activebackground=Config.getActiveColor(),
                                        command=self._analyzeResultsClicked)
         self.btnAnalyze.grid(row=0, column=0, sticky='ne', padx=15)
         # self.style.configure("W.TNotebook")#, background='white')
         tab = ttk.Notebook(infoFrame)  # , style='W.TNotebook')
 
         # Summary tab
-        dframe = tk.Frame(tab, bg='white')
+        dframe = tk.Frame(tab, bg=Config.SCIPION_BG_COLOR)
         pwgui.configureWeigths(dframe, row=0)
         pwgui.configureWeigths(dframe, row=2)
         # Just configure the provider, later below, in updateSelection, it will be
@@ -297,14 +276,14 @@ def createContent(self):
                                      self.project.mapper, self.info)
 
         rowheight = pwgui.getDefaultFont().metrics()['linespace']
-        self.style.configure("NoBorder.Treeview", background='white',
+        self.style.configure("NoBorder.Treeview", background=Config.SCIPION_BG_COLOR,
                              borderwidth=0, font=self.windows.font,
-                             rowheight=rowheight)
+                             rowheight=rowheight, fieldbackground=Config.SCIPION_BG_COLOR)
         self.infoTree = pwgui.browser.BoundTree(dframe, provider, height=6,
                                                 show='tree',
                                                 style="NoBorder.Treeview")
         self.infoTree.grid(row=0, column=0, sticky='news')
-        label = tk.Label(dframe, text='SUMMARY', bg='white',
+        label = tk.Label(dframe, text='SUMMARY', bg=Config.SCIPION_BG_COLOR,
                          font=self.windows.fontBold)
         label.grid(row=1, column=0, sticky='nw', padx=(15, 0))
 
@@ -312,7 +291,7 @@ def createContent(self):
                  'sci-bib': self._bibExportClicked}
 
         self.summaryText = pwgui.text.TaggedText(dframe, width=40, height=5,
-                                                 bg='white', bd=0,
+                                                 bg=Config.SCIPION_BG_COLOR, bd=0,
                                                  font=self.windows.font,
                                                  handlers=hView)
         self.summaryText.grid(row=2, column=0, sticky='news', padx=(30, 0))
@@ -322,11 +301,11 @@ def createContent(self):
         pwgui.configureWeigths(mframe)
         # Methods text box
         self.methodText = pwgui.text.TaggedText(mframe, width=40, height=15,
-                                                bg='white', handlers=hView)
+                                                bg=Config.SCIPION_BG_COLOR, handlers=hView)
         self.methodText.grid(row=0, column=0, sticky='news')
         # Reference export button
         # btnExportBib = pwgui.Button(mframe, text=Message.LABEL_BIB_BTN,
-        #                             fg='white', bg=Color.RED_COLOR,
+        #                             fg='white', bg=Color.MAIN_COLOR,
         #                             image=self.getImage(Icon.ACTION_BROWSE),
         #                             compound=tk.LEFT,
         #                             activeforeground='white',
@@ -526,20 +505,37 @@ def createActionToolbar(self):
         """ Prepare the buttons that will be available for protocol actions. """
 
         self.actionButtons = {}
-        self.actionList = [ACTION_EDIT, ACTION_COPY, ACTION_DELETE,
-                           ACTION_STEPS, ACTION_BROWSE, ACTION_DB,
-                           ACTION_STOP, ACTION_CONTINUE, ACTION_RESULTS,
-                           ACTION_EXPORT, ACTION_EXPORT_UPLOAD, ACTION_COLLAPSE,
-                           ACTION_EXPAND, ACTION_LABELS, ACTION_SEARCH]
+        actionList = [
+            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,
+            ACTION_EXPORT, ACTION_EXPORT_UPLOAD,
+            ACTION_COLLAPSE, ACTION_EXPAND,
+            ACTION_LABELS, ACTION_SEARCH,
+            ACTION_SELECT_FROM, ACTION_SELECT_TO,
+            ACTION_STEPS, ACTION_DB
+        ]
 
         def addButton(action, text, toolbar):
-            btn = tk.Label(toolbar, text=text,
+            btn = tk.Label(toolbar, text="",
                            image=self.getImage(ActionIcons.get(action, None)),
-                           compound=tk.LEFT, cursor='hand2', bg='white')
-            btn.bind(TK.LEFT_CLICK, lambda e: self._runActionClicked(action))
+                           compound=tk.LEFT, cursor='hand2', bg=Config.SCIPION_BG_COLOR)
+
+            callback = lambda e: self._runActionClicked(action, event=e)
+            btn.bind(TK.LEFT_CLICK, callback)
+
+            # Shortcuts:
+            shortCut = ActionShortCuts.get(action, None)
+            if shortCut:
+                text += " (%s)" % shortCut
+                self.root.bind(shortCut, callback)
+
+            ToolTip(btn,text , 500)
+
             return btn
 
-        for action in self.actionList:
+        for action in actionList:
             self.actionButtons[action] = addButton(action, action,
                                                    self.runsToolbar)
 
@@ -566,7 +562,7 @@ def addButton(action, text, toolbar):
 
     def _createViewCombo(self, parent):
         """ Create the select-view combobox. """
-        label = tk.Label(parent, text='View:', bg='white')
+        label = tk.Label(parent, text='View:', bg=Config.SCIPION_BG_COLOR)
         label.grid(row=0, column=0)
         viewChoices = ['List', 'Tree', 'Tree - small']
         self.switchCombo = pwgui.widgets.ComboBox(parent, width=10,
@@ -816,11 +812,11 @@ def createRunsGraph(self, parent):
 
         self.updateRunsGraph()
 
-    def updateRunsGraph(self, refresh=False, reorganize=False, checkPids=False):
+    def updateRunsGraph(self, refresh=False, checkPids=False):
 
         self.runsGraph = self.project.getRunsGraph(refresh=refresh,
                                                    checkPids=checkPids)
-        self.drawRunsGraph(reorganize)
+        self.drawRunsGraph()
 
     def drawRunsGraph(self, reorganize=False):
 
@@ -831,9 +827,7 @@ def drawRunsGraph(self, reorganize=False):
             self.runsGraphCanvas.reorganizeGraph(self.runsGraph, layout)
         else:
             self.runsGraphCanvas.clear()
-
-            layout = pwgui.LevelTreeLayout() if len(
-                self.settings.getNodes()) == 0 else pwgui.BasicLayout()
+            layout = pwgui.LevelTreeLayout(partial=True)
 
             # Create empty nodeInfo for new runs
             for node in self.runsGraph.getNodes():
@@ -847,6 +841,13 @@ def drawRunsGraph(self, reorganize=False):
                                            drawNode=self.createRunItem,
                                            nodeList=self.settings.nodeList)
 
+            projectSize = len(self.runsGraph.getNodes())
+            settingsNodeSize = len(self.settings.getNodes())
+            if projectSize < settingsNodeSize -1:
+                logger.info("Settings nodes list (%s) is bigger than current project nodes (%s). "
+                            "Clean up needed?" % (settingsNodeSize, projectSize) )
+                self.settings.cleanUpNodes(self.runsGraph.getNodeNames(), toRemove=False)
+
     def createRunItem(self, canvas, node):
 
         nodeId = node.run.getObjId() if node.run else 0
@@ -1335,7 +1336,7 @@ def _runItemTooltip(self, tw, item):
                 frame.grid(row=0, column=0)
                 tw.tooltipText = pwgui.dialog.createMessageBody(frame, tm, None,
                                                                 textPad=0,
-                                                                textBg=Color.LIGHT_GREY_COLOR_2)
+                                                                textBg=Color.ALT_COLOR_2)
                 tw.tooltipText.config(bd=1, relief=tk.RAISED)
             else:
                 pwgui.dialog.fillMessageText(tw.tooltipText, tm)
@@ -1614,14 +1615,12 @@ def _deleteProtocol(self):
                                  self.root):
             self.info('Deleting protocols...')
             self.project.deleteProtocol(*protocols)
+            self.settings.cleanUpNodes([str(prot.getObjId()) for prot in protocols])
             self._selection.clear()
             self._updateSelection()
             self._scheduleRunsUpdate()
             self.cleanInfo()
 
-    def _F2Pressed(self, event):
-        """ Invoked then F2 if pressed: Protocol rename"""
-        self._runActionClicked(ACTION_RENAME)
 
     def _editProtocol(self, protocol):
         disableRunMode = False
@@ -1629,7 +1628,28 @@ def _editProtocol(self, protocol):
             disableRunMode = True
         self._openProtocolForm(protocol, disableRunMode=disableRunMode)
 
-    def _copyProtocols(self):
+    def _pasteProtocolsFromClipboard(self, e=None):
+        """ Pastes the content of the clipboard providing is a json workflow"""
+
+        try:
+
+            self.project.loadProtocols(jsonStr=self.clipboard_get())
+            self.info("Clipboard content pasted successfully.")
+        except Exception as e:
+            self.info("Paste failed, maybe clipboard content is not valid content? See GUI log for details.")
+            logger.error("Clipboard content couldn't be pasted." , exc_info=e)
+
+    def _copyProtocolsToClipboard(self, e=None):
+
+        protocols = self._getSelectedProtocols()
+
+        jsonStr = self.project.getProtocolsJson(protocols)
+
+        self.clipboard_clear()
+        self.clipboard_append(jsonStr)
+        self.info("Protocols copied to the clipboard. Now you can paste them here, another project or in a template or ... anywhere!.")
+
+    def _copyProtocols(self, e=None):
         protocols = self._getSelectedProtocols()
         if len(protocols) == 1:
             newProt = self.project.copyProtocol(protocols[0])
@@ -1646,8 +1666,11 @@ def _copyProtocols(self):
     def _stopWorkFlow(self, action):
 
         protocols = self._getSelectedProtocols()
-        workflowProtocolList, activeProtList = self.project._getWorkflowFromProtocol(protocols[0],
-                                                                                     False)
+
+        # TODO: use filterCallback param and we may not need to return 2 elements
+        workflowProtocolList, activeProtList = self.project._getSubworkflow(protocols[0],
+                                                                            fixProtParam=False,
+                                                                            getStopped=False)
         if activeProtList:
             errorProtList = []
             if pwgui.dialog.askYesNo(Message.TITLE_STOP_WORKFLOW_FORM,
@@ -1672,7 +1695,7 @@ def _resetWorkFlow(self, action):
         if pwgui.dialog.askYesNo(Message.TITLE_RESET_WORKFLOW_FORM,
                                  Message.TITLE_RESET_WORKFLOW, self.root):
             self.info('Resetting the workflow...')
-            workflowProtocolList, activeProtList = self.project._getWorkflowFromProtocol(protocols[0])
+            workflowProtocolList, activeProtList = self.project._getSubworkflow(protocols[0])
             errorProtList = self.project.resetWorkFlow(workflowProtocolList)
             self.cleanInfo()
             self.refreshRuns()
@@ -1691,34 +1714,8 @@ def _launchWorkFlow(self, action):
         modes depending on the 'action' value (RESTART, CONTINUE)
         """
         protocols = self._getSelectedProtocols()
-        errorList = []
-        defaultMode = pwprot.MODE_RESUME
-        workflowProtocolList, activeProtList = self.project._getWorkflowFromProtocol(protocols[0])
-
-        # Check if exists active protocols
-        if activeProtList:
-            msg = '\n'
-            for activeProt in activeProtList:
-                msg += str(activeProt.getObjLabel()) + '\n'
-            pwgui.dialog.MessageDialog(
-                self, Message.TITLE_LAUNCHED_WORKFLOW_FAILED_FORM,
-                Message.TITLE_LAUNCHED_WORKFLOW_FAILED + "\n" +
-                Message.TITLE_ACTIVE_PROTOCOLS + "\n" + msg,
-                Icon.ERROR)
-
-        elif action == ACTION_RESTART_WORKFLOW:
-            if pwgui.dialog.askYesNo(Message.TITLE_RESTART_WORKFLOW_FORM,
-                                     Message.TITLE_RESTART_WORKFLOW, self.root):
-                defaultMode = pwprot.MODE_RESTART
-                self.info('Restarting...')
-                errorList = self.project.launchWorkflow(workflowProtocolList,
-                                                        defaultMode)
-                self.cleanInfo()
-        elif action == ACTION_CONTINUE_WORKFLOW:
-            self.info('Continuing...')
-            errorList = self.project.launchWorkflow(workflowProtocolList,
-                                                    defaultMode)
-            self.cleanInfo()
+        mode = pwprot.MODE_RESTART if action == ACTION_RESTART_WORKFLOW else pwprot.MODE_RESUME
+        errorList, _ = self._launchSubWorkflow(protocols[0], mode,  self.root)
 
         if errorList:
             msg = ''
@@ -1730,6 +1727,70 @@ def _launchWorkFlow(self, action):
                 Icon.ERROR)
         self.refreshRuns()
 
+    @staticmethod
+    def _launchSubWorkflow(protocol, mode, root, askSingleAll=False):
+        """
+        Method to launch a subworkflow
+        mode: mode value (RESTART, CONTINUE)
+        askSingleAll: specify if this method was launched from the form or from the menu
+        """
+        project = protocol.getProject()
+        workflowProtocolList, activeProtList = project._getSubworkflow(protocol)
+
+        # Check if exists active protocols
+        activeProtocols = ""
+        if activeProtList:
+            for protId, activeProt in activeProtList.items():
+                activeProtocols += ("\n* " + activeProt.getRunName())
+
+        # by default, we assume RESTART workflow option
+        title = Message.TITLE_RESTART_WORKFLOW_FORM
+        message = Message.MESSAGE_RESTART_WORKFLOW_WITH_RESULTS % ('%s\n' % activeProtocols) if len(activeProtList) else Message.MESSAGE_RESTART_WORKFLOW
+
+        if mode == pwprot.MODE_RESUME:
+             message = Message.MESSAGE_CONTINUE_WORKFLOW_WITH_RESULTS % ('%s\n' % activeProtocols) if len(activeProtList) else Message.MESSAGE_CONTINUE_WORKFLOW
+             title = Message.TITLE_CONTINUE_WORKFLOW_FORM
+
+        if not askSingleAll:
+            if pwgui.dialog.askYesNo(title,  message, root):
+                project.launchWorkflow(workflowProtocolList, mode)
+                return [], RESULT_RUN_ALL
+            return [], RESULT_CANCEL
+        else:  # launching from a form
+            if len(workflowProtocolList) > 1:
+                title = Message.TITLE_RESTART_FORM if mode == pwprot.MODE_RESTART else Message.TITLE_CONTINUE_FORM
+                message += Message.MESSAGE_ASK_SINGLE_ALL
+                result = pwgui.dialog.askSingleAllCancel(title, message,
+                                                         root)
+                if result == RESULT_RUN_ALL:
+                    if mode == pwprot.MODE_RESTART:
+                        project._restartWorkflow(workflowProtocolList)
+                    else:
+                        project._continueWorkflow(workflowProtocolList)
+
+                    return [], RESULT_RUN_ALL
+
+                elif result == RESULT_RUN_SINGLE:
+                    errorList = project.resetWorkFlow(workflowProtocolList)
+                    return errorList, RESULT_RUN_SINGLE
+
+                elif result == RESULT_CANCEL:
+                    return [], RESULT_CANCEL
+
+            else:  # is a single protocol
+                if not protocol.isSaved():
+                    title = Message.TITLE_RESTART_FORM
+                    message = Message.MESSAGE_RESTART_FORM % ('%s\n' % protocol.getRunName())
+                    if mode == pwprot.MODE_RESUME:
+                        title = Message.TITLE_CONTINUE_FORM
+                        message = Message.MESSAGE_CONTINUE_FORM % ('%s\n' % protocol.getRunName())
+
+                    result = pwgui.dialog.askYesNo(title,  message,  root)
+                    resultRun = RESULT_RUN_SINGLE if result else RESULT_CANCEL
+                    return [], resultRun
+
+                return [], RESULT_RUN_SINGLE
+
     def _selectLabels(self):
         selectedNodes = self._getSelectedNodes()
 
@@ -1927,81 +1988,94 @@ def _renameProtocol(self, prot):
         if dlg.resultYes():
             self._updateProtocol(prot)
 
-    def _runActionClicked(self, action):
-        prot = self.getSelectedProtocol()
-        if prot:
-            try:
-                if action == ACTION_DEFAULT:
-                    pass
-                elif action == ACTION_EDIT:
-                    self._editProtocol(prot)
-                elif action == ACTION_RENAME:
-                    self._renameProtocol(prot)
-                elif action == ACTION_COPY:
-                    self._copyProtocols()
-                elif action == ACTION_DELETE:
-                    self._deleteProtocol()
-                elif action == ACTION_STEPS:
-                    self._browseSteps()
-                elif action == ACTION_BROWSE:
-                    self._browseRunDirectory()
-                elif action == ACTION_DB:
-                    self._browseRunData()
-                elif action == ACTION_STOP:
-                    self._stopProtocol(prot)
-                elif action == ACTION_CONTINUE:
-                    self._continueProtocol(prot)
-                elif action == ACTION_RESULTS:
-                    self._analyzeResults(prot)
-                elif action == ACTION_EXPORT:
-                    self._exportProtocols(defaultPath=pwutils.getHomePath())
-                elif action == ACTION_EXPORT_UPLOAD:
-                    self._exportUploadProtocols()
-                elif action == ACTION_COLLAPSE:
-                    node = self.runsGraph.getNode(str(prot.getObjId()))
-                    nodeInfo = self.settings.getNodeById(prot.getObjId())
-                    nodeInfo.setExpanded(False)
-                    self.setVisibleNodes(node, visible=False)
-                    self.updateRunsGraph(True, reorganize=False)
-                    self._updateActionToolbar()
-                elif action == ACTION_EXPAND:
-                    node = self.runsGraph.getNode(str(prot.getObjId()))
-                    nodeInfo = self.settings.getNodeById(prot.getObjId())
-                    nodeInfo.setExpanded(True)
-                    self.setVisibleNodes(node, visible=True)
-                    self.updateRunsGraph(True, reorganize=False)
-                    self._updateActionToolbar()
-                elif action == ACTION_LABELS:
-                    self._selectLabels()
-                elif action == ACTION_SELECT_FROM:
-                    self._selectDescendants()
-                elif action == ACTION_SELECT_TO:
-                    self._selectAncestors()
-                elif action == ACTION_RESTART_WORKFLOW:
-                    self._launchWorkFlow(action)
-                elif action == ACTION_CONTINUE_WORKFLOW:
-                    self._launchWorkFlow(action)
-                elif action == ACTION_STOP_WORKFLOW:
-                    self._stopWorkFlow(action)
-                elif action == ACTION_RESET_WORKFLOW:
-                    self._resetWorkFlow(action)
-                elif action == ACTION_SEARCH:
-                    self._searchProtocol()
+    def _runActionClicked(self, action, event=None):
 
-            except Exception as ex:
-                self.windows.showError(str(ex))
-                if Config.debugOn():
-                    import traceback
-                    traceback.print_exc()
+        if event is not None:
+            # log Search box events are reaching here
+            # Since this method is bound to the window events
+            if event.widget.widgetName == 'entry':
+                return
 
         # Following actions do not need a select run
         if action == ACTION_TREE:
             self.drawRunsGraph(reorganize=True)
         elif action == ACTION_REFRESH:
             self.refreshRuns(checkPids=True)
+        elif action == ACTION_PASTE:
+            self._pasteProtocolsFromClipboard()
 
         elif action == ACTION_SWITCH_VIEW:
             self.switchRunsView()
+        else:
+            prot = self.getSelectedProtocol()
+            if prot:
+                try:
+                    if action == ACTION_DEFAULT:
+                        pass
+                    elif action == ACTION_EDIT:
+                        self._editProtocol(prot)
+                    elif action == ACTION_RENAME:
+                        self._renameProtocol(prot)
+                    elif action == ACTION_DUPLICATE:
+                        self._copyProtocols()
+                    elif action == ACTION_COPY:
+                        self._copyProtocolsToClipboard()
+                    elif action == ACTION_DELETE:
+                        self._deleteProtocol()
+                    elif action == ACTION_STEPS:
+                        self._browseSteps()
+                    elif action == ACTION_BROWSE:
+                        self._browseRunDirectory()
+                    elif action == ACTION_DB:
+                        self._browseRunData()
+                    elif action == ACTION_STOP:
+                        self._stopProtocol(prot)
+                    elif action == ACTION_CONTINUE:
+                        self._continueProtocol(prot)
+                    elif action == ACTION_RESULTS:
+                        self._analyzeResults(prot)
+                    elif action == ACTION_EXPORT:
+                        self._exportProtocols(defaultPath=pwutils.getHomePath())
+                    elif action == ACTION_EXPORT_UPLOAD:
+                        self._exportUploadProtocols()
+                    elif action == ACTION_COLLAPSE:
+                        node = self.runsGraph.getNode(str(prot.getObjId()))
+                        nodeInfo = self.settings.getNodeById(prot.getObjId())
+                        nodeInfo.setExpanded(False)
+                        self.setVisibleNodes(node, visible=False)
+                        self.updateRunsGraph(True)
+                        self._updateActionToolbar()
+                    elif action == ACTION_EXPAND:
+                        node = self.runsGraph.getNode(str(prot.getObjId()))
+                        nodeInfo = self.settings.getNodeById(prot.getObjId())
+                        nodeInfo.setExpanded(True)
+                        self.setVisibleNodes(node, visible=True)
+                        self.updateRunsGraph(True)
+                        self._updateActionToolbar()
+                    elif action == ACTION_LABELS:
+                        self._selectLabels()
+                    elif action == ACTION_SELECT_FROM:
+                        self._selectDescendants()
+                    elif action == ACTION_SELECT_TO:
+                        self._selectAncestors()
+                    elif action == ACTION_RESTART_WORKFLOW:
+                        self._launchWorkFlow(action)
+                    elif action == ACTION_CONTINUE_WORKFLOW:
+                        self._launchWorkFlow(action)
+                    elif action == ACTION_STOP_WORKFLOW:
+                        self._stopWorkFlow(action)
+                    elif action == ACTION_RESET_WORKFLOW:
+                        self._resetWorkFlow(action)
+                    elif action == ACTION_SEARCH:
+                        self._searchProtocol()
+
+                except Exception as ex:
+                    self.windows.showError(str(ex))
+                    if Config.debugOn():
+                        import traceback
+                        traceback.print_exc()
+            else:
+                self.info("Action '%s' not implemented." % action)
 
     def setVisibleNodes(self, node, visible=True):
         hasParentHidden = False
diff --git a/pyworkflow/gui/project/viewprotocols_extra.py b/pyworkflow/gui/project/viewprotocols_extra.py
index de122b760..2a70657fa 100644
--- a/pyworkflow/gui/project/viewprotocols_extra.py
+++ b/pyworkflow/gui/project/viewprotocols_extra.py
@@ -144,7 +144,7 @@ def viewerCallback(viewer):
             return lambda: self._visualizeObject(viewer, obj)
 
         for v in viewers:
-            actions.append(('Open with %s' % v.__name__,
+            actions.append(('Open with %s' % v.__name__ if v._name is None else v._name,
                             viewerCallback(v),
                             Icon.ACTION_VISUALIZE))
         # EDIT
@@ -156,7 +156,7 @@ def viewerCallback(viewer):
         # since we can end up with several outputs and
         # we may want to clean up
         if self.protocol.allowsDelete(obj) and not isPointer:
-            actions.append((Message.LABEL_DELETE_ACTION,
+            actions.append((Message.LABEL_DELETE,
                             lambda: self._deleteObject(obj),
                             Icon.ACTION_DELETE))
         return actions
@@ -400,7 +400,7 @@ def __checkItem(cls, item):
         if protClass is not None:
             if protClass.isBeta():
                 icon = Icon.BETA
-            elif protClass.isNew():
+            elif protClass.isNewDev():
                 icon = Icon.NEW
             elif protClass.isUpdated():
                 icon = Icon.UPDATED
@@ -412,16 +412,17 @@ def __addAllProtocols(cls, domain, protocols):
         # Add all protocols
         allProts = domain.getProtocols()
 
-        # Sort the dictionary
-        allProtsSorted = pwobj.OrderedDict(sorted(allProts.items(),
-                                            key=lambda e: e[1].getClassLabel()))
+        # Sort the list
+        allProtsSorted = sorted(allProts.items(), key=lambda e: e[1].getClassLabel())
+
         allProtMenu = ProtocolConfig(cls.ALL_PROTOCOLS)
         packages = {}
 
         # Group protocols by package name
-        for k, v in allProtsSorted.items():
+        for k, v in allProtsSorted:
             if isAFinalProtocol(v, k):
-                packageName = v.getClassPackageName()
+                packageName = v.getPlugin().getName()
+
                 # Get the package submenu
                 packageMenu = packages.get(packageName)
 
@@ -436,7 +437,7 @@ def __addAllProtocols(cls, domain, protocols):
                     packages[packageName] = packageMenu
 
                 # Add the protocol
-                tag = cls.getProtocolTag(v.isInstalled(), v.isBeta(), v.isNew(), v.isUpdated())
+                tag = cls.getProtocolTag(v.isInstalled(), v.isBeta(), v.isNewDev(), v.isUpdated())
 
                 protLine = {"tag": tag, "value": k,
                             "text": v.getClassLabel(prependPackageName=False)}
diff --git a/pyworkflow/gui/text.py b/pyworkflow/gui/text.py
index 7b196ab26..76053943b 100644
--- a/pyworkflow/gui/text.py
+++ b/pyworkflow/gui/text.py
@@ -36,8 +36,9 @@
 import tkinter.messagebox as tkMessageBox
 
 import pyworkflow as pw
+from pyworkflow import ASCII_COLOR_2_TKINTER
 from pyworkflow.utils import (HYPER_BOLD, HYPER_ITALIC, HYPER_LINK1, HYPER_LINK2,
-                              parseHyperText, renderLine, renderTextFile, colorName,
+                              parseHyperText, renderLine, renderTextFile,
                               which, envVarOn, expandPattern)
 from pyworkflow.utils.properties import Message, Color, Icon
 from . import gui
@@ -112,7 +113,7 @@ class HyperlinkManager:
     http://effbot.org/zone/tkinter-text-hyperlink.htm """
     def __init__(self, text):
         self.text = text
-        self.text.tag_config("hyper", foreground=Color.RED_COLOR, underline=1)
+        self.text.tag_config("hyper", foreground=pw.Config.SCIPION_MAIN_COLOR, underline=1)
         self.text.tag_bind("hyper", "<Enter>", self._enter)
         self.text.tag_bind("hyper", "<Leave>", self._leave)
         self.text.tag_bind("hyper", "<Button-1>", self._click)
@@ -312,7 +313,7 @@ def highlight(self, pattern, tag, start="1.0", end="end", regexp=False):
 def configureColorTags(text):
     """ Create tags in text (of type tk.Text) for all the supported colors. """
     try:
-        for color in colorName.values():
+        for color in ASCII_COLOR_2_TKINTER.values():
             text.tag_config(color, foreground=color)
         return True
     except Exception as e:
@@ -335,7 +336,7 @@ def __init__(self, master, colors=True, **opts):
         self.hm = HyperlinkManager(self)
 
     def getDefaults(self):
-        return {'bg': "white", 'bd': 0}
+        return {'bg': pw.Config.SCIPION_BG_COLOR, 'bd': 0}
         # It used to have also 'font': gui.fontNormal  but that stops
         # this file from running. Apparently there is no fontNormal in gui.
 
@@ -485,7 +486,7 @@ def doRefresh(self):
 class TextFileViewer(tk.Frame):
     """ Implementation of a simple text file viewer """
     
-    LabelBgColor = "white"
+    # Not used? --> LabelBgColor = "white"
     
     def __init__(self, master, fileList=[],
                  allowSearch=True, allowRefresh=True, allowOpen=False,
@@ -557,9 +558,10 @@ def createWidgets(self, fileList):
             self.searchEntry = tk.Entry(right, textvariable=self.searchVar,
                                         font=self._font)
             self.searchEntry.grid(row=0, column=4, sticky='ew', padx=5)
-            self.searchEntry.bind('<Return>', self.findText)
-            self.searchEntry.bind('<KP_Enter>', self.findText)
-            # btn = IconButton(right, "Search", Icon.ACTION_SEARCH,
+
+            # self.searchEntry.bind('<Return>', self.findText)
+            # self.searchEntry.bind('<KP_Enter>', self.findText)
+            # # btn = IconButton(right, "Search", Icon.ACTION_SEARCH,
             #                  tooltip=Message.TOOLTIP_SEARCH,
             #                  command=self.findText, bg=None)
             # btn.grid(row=0, column=5, padx=(0, 5))
@@ -614,6 +616,8 @@ def addBinding(self):
                                (lambda e: self.findText(matchCase=True), "Trigger a case sensitive search", ['<Shift-Return>']),
                                (lambda e: self.findText(), "Move to the next highlighted item", ["<Down>", '<F3>']),
                                (lambda e: self.findText(-1), "Move to the previous highlighted item", ["<Up>", '<Shift-F3>']),
+                               (lambda e: self.modifyFontSize(pw.Config.SCIPION_FONT_SIZE + 2), "Increase the font size",["<Control-KP_Add>"]),
+                               (lambda e: self.modifyFontSize(pw.Config.SCIPION_FONT_SIZE), "Increase the font size",["<Control-KP_Subtract>"]),
                                ]
         tooltip = "Shortcuts:"
 
@@ -675,6 +679,10 @@ def changePosition(self, index):
     def findPrevText(self):
         self.findText(-1)
 
+    def modifyFontSize(self, newSize):
+        text = self.selectedText()
+        text['font']=(None, newSize)
+
     def findText(self, direction=1, matchCase=0):
         text = self.selectedText()
         str = self.searchVar.get()
diff --git a/pyworkflow/gui/tree.py b/pyworkflow/gui/tree.py
index e30b49eb2..816d1fec5 100644
--- a/pyworkflow/gui/tree.py
+++ b/pyworkflow/gui/tree.py
@@ -30,7 +30,6 @@
 import tkinter as tk
 import tkinter.ttk as ttk
 
-from pyworkflow.object import Scalar
 from pyworkflow.mapper import SqliteMapper
 from pyworkflow.utils import prettyDelta
 from . import gui
diff --git a/pyworkflow/gui/widgets.py b/pyworkflow/gui/widgets.py
index 6e3d3359e..f48f9b438 100644
--- a/pyworkflow/gui/widgets.py
+++ b/pyworkflow/gui/widgets.py
@@ -32,7 +32,9 @@
 
 from math import ceil
 
-from pyworkflow import TK_GRAY_DEFAULT
+import pyworkflow
+from pyworkflow import TK_GRAY_DEFAULT, Config
+
 from . import gui
 from .tooltip import ToolTip
 
@@ -85,7 +87,7 @@ class IconButton(HotButton):
     """ Hot button, but only with image and no border """
 
     def __init__(self, master, text, imagePath, tooltip=None, **opts):
-        defaults = {'bd': 0, 'bg': 'white', 'compound': tk.NONE}
+        defaults = {'bd': 0, 'bg': Config.SCIPION_BG_COLOR, 'compound': tk.NONE}
         defaults.update(opts)
         HotButton.__init__(self, master, text, imagePath, tooltip, **defaults)
 
@@ -279,8 +281,8 @@ class GradientFrame(tk.Canvas):
 
     def __init__(self, parent, **args):
         tk.Canvas.__init__(self, parent, **args)
-        self._color1 = "#d2a7a7"
-        self._color2 = "#820808"
+        self._color1 = Config.SCIPION_BG_COLOR #"#d2a7a7"
+        self._color2 = Config.SCIPION_MAIN_COLOR #"#820808"
         self.bind("<Configure>", self._draw_gradient)
 
     def _draw_gradient(self, event=None):
diff --git a/pyworkflow/object.py b/pyworkflow/object.py
index 139041907..ce464f807 100644
--- a/pyworkflow/object.py
+++ b/pyworkflow/object.py
@@ -28,6 +28,9 @@
 The Object class is the root in the hierarchy and some other
 basic classes.
 """
+import logging
+logger = logging.getLogger(__name__)
+
 from collections import OrderedDict
 import datetime as dt
 from os.path import getmtime
@@ -54,7 +57,7 @@
 
 class Object(object):
     """ All objects in our Domain should inherit from this class
-    that will contains all base properties"""
+    that will contain all base properties"""
 
     def __init__(self, value=None, **kwargs):
         object.__init__(self)
@@ -157,11 +160,10 @@ def getAttributesToStore(self):
             try:
                 if attr is not None and attr._objDoStore:
                     yield key, attr
-            except:
-                print("Object.getAttributesToStore: attribute '%s' seems to "
-                      "be overwritten," % key)
-                print("   since '_objDoStore' was not found. "
-                      "Ignoring attribute. ")
+            except Exception as e:
+                logger.info("Object.getAttributesToStore: attribute '%s' (%s) seems to "
+                      "be overwritten, since '_objDoStore' was not found. Ignoring attribute." % (key, type(attr)))
+                logger.debug("Exception: %s" % e)
 
     def isPointer(self):
         """If this is true, the value field is a pointer 
@@ -341,9 +343,7 @@ def equalAttributes(self, other, ignore=[], verbose=False):
                 comp = v1 == v2
             if not comp:
                 if verbose:
-                    print("Different attributes: ")
-                    print("self.%s = %s" % (k, v1))
-                    print("other.%s = %s" % (k, v2))
+                    logger.info("Different attributes: self.%s = %s, other.%s = %s" % (k, v1, k, v2))
                 return False
         return True
             
@@ -559,14 +559,14 @@ def printAll(self, name=None, level=0):
         tab = ' ' * (level*3)
         idStr = ''  # ' (id = %s, pid = %s)' % (self.getObjId(), self._objParentId)
         if name is None:
-            print(tab, self.getClassName(), idStr)
+            logger.info(tab, self.getClassName(), idStr)
         else:
             if name == 'submitTemplate':  # Skip this because very large value
                 value = '...'
             else:
                 value = self.getObjValue()
                 
-            print(tab, '%s = %s' % (name, value), idStr)
+            logger.info(tab, '%s = %s' % (name, value), idStr)
         for k, v in self.getAttributes():
             v.printAll(k, level + 1)
             
@@ -1042,7 +1042,12 @@ def _convertValue(self, value):
             else:
                 raise Exception("CsvList.set: Invalid value type: ",
                                 type(value))
-            
+
+    def equalAttributes(self, other, ignore=[], verbose=False):
+        """Compare that all attributes are equal"""
+
+        return self == other
+
     def getObjValue(self):
         self._objValue = ','.join(map(str, self))
         return self._objValue
@@ -1069,7 +1074,7 @@ def __eq__(self, other):
 class Set(Object):
     """ This class will be a container implementation for elements.
     It will use an extra sqlite file to store the elements.
-    All items will have an unique id that identifies each element in the set.
+    All items will have a unique id that identifies each element in the set.
     """
     ITEM_TYPE = None  # This property should be defined to know the item type
     
@@ -1101,7 +1106,22 @@ def __init__(self, filename=None, prefix='',
         if filename:
             self._mapperPath.set('%s, %s' % (filename, prefix))
             self.load()
-            
+
+    def copy(self, other, copyId=True, ignoreAttrs=['_mapperPath', '_size', '_streamState']):
+        """ Copies the attributes of the set
+
+        :param other: Set to copy attributes from
+        :param copyId: True. Copies the objId.
+        :param ignoreAttrs: Attributes list to ignore while copying. _mapperPath, _size and _streamState
+        are ignored by default.
+        
+        """
+        
+        # Note: By default this behaves properly for cloning non identical objects. 
+        # This is to clone a set from another new set that is based on "other".
+        # For exact clone, we need to pass empty ignoreAttrs. This case happens in Protocol.__tryUpdateOutputSet
+        super().copy(other, copyId, ignoreAttrs)
+
     def _getMapper(self):
         """ This method will open the connection to the items
         database on demand. That's why this method should 
@@ -1171,8 +1191,8 @@ def iterItems(self, orderBy='id', direction='ASC', where=None,
     def getFirstItem(self):
         """ Return the first item in the Set. """
         # This function is used in many contexts where the mapper can be
-        # left open and could be problematic locking other db
-        # So, we looking if the mapper was closed before, in which case
+        # left open and could be problematic locking the db for other processes
+        # So, we look if the mapper was closed before, in which case
         # we will close it after that
         closedMapper = self._mapper is None
         firstItem = self._getMapper().selectFirst()
diff --git a/pyworkflow/plugin.py b/pyworkflow/plugin.py
index acd09ccef..fe54b30f1 100644
--- a/pyworkflow/plugin.py
+++ b/pyworkflow/plugin.py
@@ -27,6 +27,7 @@
 # *
 # **************************************************************************
 import logging
+import sys
 
 from .protocol import Protocol
 from .viewer import Viewer
@@ -84,6 +85,8 @@ def registerPlugin(cls, name):
 
             # Define variables
             m.Plugin._defineVariables()
+            m._pluginInstance = m.Plugin()
+
             m.Domain = cls  # Register the domain class for this module
             # TODO avoid loading bibtex here and make a lazy load like the rest.
             # Load bibtex
@@ -186,7 +189,7 @@ def get_recursive_modules(module):
 
     @classmethod
     def __getSubclasses(cls, submoduleName, BaseClass,
-                        updateBaseClasses=False):
+                        updateBaseClasses=False, setPackage=False):
         """ Load all detected subclasses of a given BaseClass.
         Params:
             updateBaseClasses: if True, it will try to load classes from the
@@ -203,7 +206,7 @@ def __getSubclasses(cls, submoduleName, BaseClass,
                         if inspect.isclass(attr) and issubclass(attr, BaseClass):
                             cls._baseClasses[name] = attr
 
-            for pluginName, plugin in cls.getPlugins().items():
+            for pluginName, module in cls.getPlugins().items():
                 sub = cls.__getSubmodule(pluginName, submoduleName)
                 if sub is not None:
                     for name in cls.getModuleClasses(sub):
@@ -222,7 +225,12 @@ def __getSubclasses(cls, submoduleName, BaseClass,
                                        pluginCollision))
                             else:
                                 # Set this special property used by Scipion
-                                attr._package = plugin
+                                # Protocols need the package to be set
+                                if setPackage:
+
+                                    attr._plugin = getattr(module, "_pluginInstance", None)
+                                    attr._package = module
+
                                 subclasses[name] = attr
             subclasses.update(
                 pwutils.getSubclasses(BaseClass, cls._baseClasses))
@@ -246,7 +254,7 @@ def getModuleClasses(cls, module):
     @classmethod
     def getProtocols(cls):
         """ Return all Protocol subclasses from all plugins for this domain."""
-        return cls.__getSubclasses('protocols', cls._protocolClass)
+        return cls.__getSubclasses('protocols', cls._protocolClass, setPackage=True)
 
     @classmethod
     def getObjects(cls):
@@ -262,7 +270,7 @@ def viewersLoaded(cls):
     def getViewers(cls):
         """ Return all Viewer subclasses from all plugins for this domain."""
         return cls.__getSubclasses('viewers', cls._viewerClass,
-                                   updateBaseClasses=True)
+                                   updateBaseClasses=True, setPackage=True)
 
     @classmethod
     def getWizards(cls):
@@ -521,10 +529,14 @@ class Plugin:
     _homeVar = ''  # Change in subclasses to define the "home" variable
     _pathVars = []
     _supportedVersions = []
-    _name = ""
     _url = ""  # For the plugin
     _condaActivationCmd = None
 
+    def __init__(self):
+        self._path = None
+        self._inDevelMode = None
+        self._name = None
+
     @classmethod
     def _defineVar(cls, varName, defaultValue):
         """ Internal method to define variables trying to get it from the environment first. """
@@ -538,7 +550,7 @@ def _addVar(cls, varName, value):
     @classmethod
     @abstractmethod
     def getEnviron(cls):
-        """ Setup the environment variables needed to launch programs. """
+        """ Set up the environment variables needed to launch programs. """
         pass
 
     @classmethod
@@ -613,10 +625,6 @@ def getActiveVersion(cls, home=None, versions=None):
 
         return ''
 
-    @classmethod
-    def getName(cls):
-        return cls.__module__
-
     @classmethod
     def validateInstallation(cls):
         """
@@ -637,23 +645,32 @@ def validateInstallation(cls):
             return ["validateInstallation fails: %s" % e]
 
     @classmethod
-    def getPluginDir(cls):
-        return os.path.join(pw.getModuleFolder(cls.getName()))
+    def getUrl(cls, protClass=None):
+        """ Url for the plugin to point users to it"""
+        return cls._url
 
-    @classmethod
-    def getPluginTemplateDir(cls):
-        return os.path.join(cls.getPluginDir(), 'templates')
+    # -------------- Instance methods ----------------
+    def getName(self):
+        if self._name is None:
+            self._name=  self.__class__.__module__
 
+        return self._name
 
-    @classmethod
-    def getTemplates(cls):
+    def getPluginDir(self):
+        return self.getPath()
+
+    def getPluginTemplateDir(self):
+        return os.path.join(self.getPath(), 'templates')
+
+
+    def getTemplates(self):
         """ Get the plugin templates from the templates directory.
             If more than one template is found or passed, a dialog is raised
             to choose one.
         """
         tempList = []
-        pluginName = cls.getName()
-        tDir = cls.getPluginTemplateDir()
+        pluginName = self.getName()
+        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))
@@ -661,10 +678,16 @@ def getTemplates(cls):
 
         return tempList
 
-    @classmethod
-    def getUrl(cls, protClass=None):
-        """ Url for the plugin to point users to it"""
-        return cls._url
+    def getPath(self):
+        if self._path is None:
+            self._path = sys.modules[self.__class__.__module__].__path__[0]
+
+        return self._path
+    def inDevelMode(self)-> bool:
+        """ Returns true if code is not in python's site-packages folder"""
+        if self._inDevelMode is None:
+            self._inDevelMode = pwutils.getPythonPackagesFolder() not in self.getPath()
+        return self._inDevelMode
 
 
 class PluginInfo:
diff --git a/pyworkflow/project/config.py b/pyworkflow/project/config.py
index dc51c74f5..36944a828 100644
--- a/pyworkflow/project/config.py
+++ b/pyworkflow/project/config.py
@@ -24,12 +24,10 @@
 # *
 # **************************************************************************
 
-
-import os
+import logging
+logger = logging.getLogger(__name__)
 import json
 import datetime as dt
-from collections import OrderedDict
-from configparser import ConfigParser
 
 import pyworkflow.object as pwobj
 from pyworkflow.mapper import SqliteMapper
@@ -148,12 +146,43 @@ def write(self, dbPath=None):
     def getNodes(self):
         return self.nodeList
 
+    def cleanUpNodes(self, runsIds, toRemove=True):
+        """ This will clean up all the nodes that do not have a matching run.
+        This is because until now, the nodes here weren't removes when protocols were removed.
+
+        :param runsIds: iterable with protocol's objId to be removed.
+        :param toRemove: Passed is are to be removed. Otherwise, are the ones to keep
+        """
+
+        try:
+            logger.info("Cleaning up unused graphical nodes.")
+
+            nodesToDelete = []
+            for node  in self.getNodes():
+                nodeId = str(node.getId())
+                # if it is not the root node
+                if nodeId != '0':
+
+                    if (nodeId in runsIds) == toRemove:
+                        nodesToDelete.append(node.getId())
+
+            logger.info("Following graphical nodes %s unmatched. Deleting them" % nodesToDelete)
+            for key in nodesToDelete:
+                self.getNodes().removeNode(key)
+                
+        except Exception as e:
+            logger.error("Couldn't clean up graphical nodes.", exc_info=e)
+
     def getNodeById(self, nodeId):
         return self.nodeList.getNode(nodeId)
 
     def addNode(self, nodeId, **kwargs):
         return self.nodeList.addNode(nodeId, **kwargs)
 
+    def removeNode(self, nodeId):
+        """ Removes a graphical node based on its id"""
+        self.nodeList.removeNode(nodeId)
+
     def getLabels(self):
         return self.labelsList
 
@@ -328,6 +357,12 @@ def addNode(self, nodeId, **kwargs):
         self.append(node)
         return node
 
+    def removeNode(self, nodeId):
+        """ Removes a node with the id = nodeId"""
+        nodeToRemove = self._nodesDict[nodeId]
+        self._nodesDict.pop(nodeId)
+        self.remove(nodeToRemove)
+
     def updateDict(self):
         self._nodesDict.clear()
         for node in self:
diff --git a/pyworkflow/project/project.py b/pyworkflow/project/project.py
index 22e8e69ec..8e4ab35ef 100644
--- a/pyworkflow/project/project.py
+++ b/pyworkflow/project/project.py
@@ -25,6 +25,8 @@
 # *
 # **************************************************************************
 import logging
+
+ROOT_NODE_NAME = "PROJECT"
 logger = logging.getLogger(__name__)
 
 import datetime as dt
@@ -412,6 +414,8 @@ def _continueWorkflow(self, continuedProtList=None, errorsList=None):
         if continuedProtList is not None:
             for protocol, level in continuedProtList.values():
                 if not protocol.isInteractive():
+                    if protocol.isScheduled():
+                        continue
                     if protocol.worksInStreaming():
                         attrSet = [attr for name, attr in
                                    protocol.iterOutputAttributes(pwprot.Set)]
@@ -437,6 +441,8 @@ def _continueWorkflow(self, continuedProtList=None, errorsList=None):
                         if level != 0:
                             # we make sure that at least one protocol in streaming
                             # has been launched
+                            if protocol.isActive():
+                                self.stopProtocol(protocol)
                             self._restartWorkflow({protocol.getObjId(): (protocol, level)},
                                                   errorsList)
 
@@ -461,6 +467,10 @@ def _restartWorkflow(self, restartedProtList=None, errorsList=None):
             for protocol, level in restartedProtList.values():
                 if not protocol.isInteractive():
                     try:
+                        if protocol.isScheduled():
+                            continue
+                        elif protocol.isActive():
+                            self.stopProtocol(protocol)
                         protocol.runMode.set(MODE_RESTART)
                         self.scheduleProtocol(protocol,
                                               initialSleepTime=level*INITIAL_SLEEP_TIME)
@@ -510,7 +520,7 @@ def stopWorkFlow(self, activeProtList):
         :param initialProtocol: selected protocol
         """
         errorProtList = []
-        for protocol in activeProtList:
+        for protocol in activeProtList.values():
             try:
                 self.stopProtocol(protocol)
             except Exception:
@@ -819,38 +829,51 @@ def _checkModificationAllowed(self, protocols, msg):
 
         self._checkProtocolsDependencies(protocols, msg)
 
-    def _getWorkflowFromProtocol(self, protocol, fixProtParam=True):
+    def _getSubworkflow(self, protocol, fixProtParam=True, getStopped=True):
         """
         This function get the workflow from "protocol" and determine the
         protocol level into the graph. Also, checks if there are active
         protocols excluding interactive protocols.
+        :param protocol from where to start the subworkflow (included)
+        :param fixProtParam fix the old parameters configuration in the protocols
+        :param getStopped takes into account protocols that aren't stopped
         """
-        activeProtList = []
-        configuredProtList = {}
+        affectedProtocols = {}
+        affectedProtocolsActive = {}
         auxProtList = []
         # store the protocol and your level into the workflow
-        configuredProtList[protocol.getObjId()] = [protocol, 0]
-        auxProtList.append(protocol.getObjId())
+        affectedProtocols[protocol.getObjId()] = [protocol, 0]
+        auxProtList.append([protocol.getObjId(), 0])
         runGraph = self.getRunsGraph()
 
         while auxProtList:
-            protocol = runGraph.getNode(str(auxProtList.pop(0))).run
-            level = configuredProtList[protocol.getObjId()][1] + 1
+            protId, level = auxProtList.pop(0)
+            protocol = runGraph.getNode(str(protId)).run
+
+            # Increase the level for the children
+            level = level + 1
+
             if fixProtParam:
                 self._fixProtParamsConfiguration(protocol)
-            if protocol.isActive() and protocol.getStatus() != STATUS_INTERACTIVE:
-                activeProtList.append(protocol)
+
+            if not getStopped and protocol.isActive():
+                affectedProtocolsActive[protocol.getObjId()] = protocol
+            elif not protocol.getObjId() in affectedProtocolsActive.keys() and getStopped and \
+                    not protocol.isSaved() and protocol.getStatus() != STATUS_INTERACTIVE:
+                affectedProtocolsActive[protocol.getObjId()] = protocol
+
             node = runGraph.getNode(protocol.strId())
             dependencies = [node.run for node in node.getChilds()]
             for dep in dependencies:
                 if not dep.getObjId() in auxProtList:
-                    auxProtList.append(dep.getObjId())
-                if not dep.getObjId() in configuredProtList.keys():
-                    configuredProtList[dep.getObjId()] = [dep, level]
-                elif level > configuredProtList[dep.getObjId()][1]:
-                    configuredProtList[dep.getObjId()][1] = level
+                    auxProtList.append([dep.getObjId(), level])
+
+                if not dep.getObjId() in affectedProtocols.keys():
+                    affectedProtocols[dep.getObjId()] = [dep, level]
+                elif level > affectedProtocols[dep.getObjId()][1]:
+                    affectedProtocols[dep.getObjId()][1] = level
 
-        return configuredProtList, activeProtList
+        return affectedProtocols, affectedProtocolsActive
 
     def deleteProtocol(self, *protocols):
         self._checkModificationAllowed(protocols, 'Cannot DELETE protocols')
@@ -1136,14 +1159,22 @@ def loadProtocols(self, filename=None, jsonStr=None):
         """ Load protocols generated in the same format as self.exportProtocols.
 
         :param filename: the path of the file where to read the workflow.
-        :param jsonStr: Not used.
+        :param jsonStr:
 
         Note: either filename or jsonStr should be not None.
 
         """
-        f = open(filename)
-        importDir = os.path.dirname(filename)
-        protocolsList = json.load(f)
+        importDir = None
+        if filename:
+            with open(filename) as f:
+                importDir = os.path.dirname(filename)
+                protocolsList = json.load(f)
+
+        elif jsonStr:
+            protocolsList = json.loads(jsonStr)
+        else:
+            logger.error("Invalid call to  loadProcols. Either filename or jsonStr has to be passed.")
+            return
 
         emProtocols = self._domain.getProtocols()
         newDict = OrderedDict()
@@ -1161,7 +1192,7 @@ def loadProtocols(self, filename=None, jsonStr=None):
                 prot = self.newProtocol(protClass,
                                         objLabel=protLabel,
                                         objComment=protDict.get('object.comment', None))
-                protocolsList[i] = prot.processImportDict(protDict, importDir)
+                protocolsList[i] = prot.processImportDict(protDict, importDir) if importDir else protDict
 
                 prot._useQueue.set(protDict.get('_useQueue', False))
                 prot._queueParams.set(protDict.get('_queueParams', None))
@@ -1230,7 +1261,6 @@ def _setPrerequisites(prot):
 
                 self.mapper.store(prot)
 
-        f.close()
         self.mapper.commit()
 
         return newDict
@@ -1444,12 +1474,19 @@ def getGraphFromRuns(self, runs):
 
         """
         outputDict = {}  # Store the output dict
-        g = pwutils.Graph(rootName='PROJECT')
+        g = pwutils.Graph(rootName=ROOT_NODE_NAME)
 
         for r in runs:
             n = g.createNode(r.strId())
             n.run = r
-            n.setLabel(r.getRunName())
+
+            # Legacy protocols do not have a plugin!!
+            develTxt =''
+            plugin=r.getPlugin()
+            if plugin and plugin.inDevelMode():
+                develTxt='ẟ '
+
+            n.setLabel('%s%s' % (develTxt , r.getRunName()))
             outputDict[r.getObjId()] = n
             for _, attr in r.iterOutputAttributes():
                 # mark this output as produced by r
@@ -1482,7 +1519,7 @@ def _checkInputAttr(node, pointed):
                         _checkInputAttr(node, parent)
         rootNode = g.getRoot()
         rootNode.run = None
-        rootNode.label = "PROJECT"
+        rootNode.label = ROOT_NODE_NAME
 
         for n in g.getNodes():
             if n.isRoot() and n is not rootNode:
@@ -1493,7 +1530,7 @@ def _getRelationGraph(self, relation=pwobj.RELATION_SOURCE, refresh=False):
         """ Retrieve objects produced as outputs and
         make a graph taking into account the SOURCE relation. """
         relations = self.mapper.getRelationsByName(relation)
-        g = pwutils.Graph(rootName='PROJECT')
+        g = pwutils.Graph(rootName=ROOT_NODE_NAME)
         root = g.getRoot()
         root.pointer = None
         runs = self.getRuns(refresh=refresh)
diff --git a/pyworkflow/project/scripts/clean_projects.py b/pyworkflow/project/scripts/clean_projects.py
index ee0ffaeae..d563699d5 100755
--- a/pyworkflow/project/scripts/clean_projects.py
+++ b/pyworkflow/project/scripts/clean_projects.py
@@ -35,8 +35,8 @@ def usage(error):
     print("""
     ERROR: %s
     
-    Usage: scipion python scripts/clean_projects.py [SCIPION_USER_DATA] [--delete]
-        Clean projects that has expired (now - creation time > life time)
+    Usage: scipion python -m pyworkflow.project.scripts.clean_projects [SCIPION_USER_DATA] [--delete]
+        Clean projects that have expired (now - creation time > life time)
         Optional to pass SCIPION_USER_DATA folder from which to read 'projects'.
         If --delete is not passed, only a message with the projects to be deleted
         will be shown. If used --delete, the projects will be deleted from filesystem.
diff --git a/pyworkflow/project/scripts/config.py b/pyworkflow/project/scripts/config.py
index 490a0aeda..8f9eaf8a2 100755
--- a/pyworkflow/project/scripts/config.py
+++ b/pyworkflow/project/scripts/config.py
@@ -38,10 +38,10 @@ def usage(error):
     print("""
     ERROR: %s
     
-    Usage: scipion python scripts/config_projects.py ProjectName [readOnly=True|False] [lifeTime=X|None]
-        This script show (or edit) 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 life time of the project.
+    Usage: scipion python -m pyworkflow.project.scripts.config_projects 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.
     """ % error)
     sys.exit(1)    
 
diff --git a/pyworkflow/project/scripts/create.py b/pyworkflow/project/scripts/create.py
index a8ceed188..ace576704 100644
--- a/pyworkflow/project/scripts/create.py
+++ b/pyworkflow/project/scripts/create.py
@@ -36,10 +36,11 @@ def usage(error):
     print("""
     ERROR: %s
 
-    Usage: scipion3 python -m pyworkflow.project.scripts.create name 
-        name="project name"
-        [workflow="file"] path to a Scipion json workflow
-        [location="folder"] where to create it, defaults to scipion default location
+    Usage: scipion python -m pyworkflow.project.scripts.create NAME [WORKFLOW] [LOCATION]
+        NAME: project name
+        WORKFLOW: path to a Scipion json workflow
+        LOCATION: where to create it, defaults to scipion default location
+
         This script will create a project project, optionally based on a workflow file
     """ % error)
     sys.exit(1)
diff --git a/pyworkflow/project/scripts/edit_workflow.py b/pyworkflow/project/scripts/edit_workflow.py
index 3051d688d..66160a2f2 100644
--- a/pyworkflow/project/scripts/edit_workflow.py
+++ b/pyworkflow/project/scripts/edit_workflow.py
@@ -40,7 +40,7 @@ def usage(error):
     print("""
     ERROR: %s
     
-    Usage: python -m pyworkflow.project.scripts.edit_workflow workflow.json
+    Usage: scipion python -m pyworkflow.project.scripts.edit_workflow workflow.json
         Edit the provide json file with scipion workflow.
         It will create a project, import the workflow and save
         the workflow back before closing the project.
diff --git a/pyworkflow/project/scripts/fix_links.py b/pyworkflow/project/scripts/fix_links.py
index 000a595f0..e02b0f82d 100755
--- a/pyworkflow/project/scripts/fix_links.py
+++ b/pyworkflow/project/scripts/fix_links.py
@@ -13,10 +13,9 @@ def usage(error):
     print("""
     ERROR: %s
     
-    Usage: python -m pyworkflow.project.script.fix_links PROJECT SEARCH_DIR
-        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.    
+    Usage: scipion python -m pyworkflow.project.scripts.fix_links PROJECT SEARCH_DIR
+        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)
 
diff --git a/pyworkflow/project/scripts/load.py b/pyworkflow/project/scripts/load.py
index c1a112658..6f81cc65c 100644
--- a/pyworkflow/project/scripts/load.py
+++ b/pyworkflow/project/scripts/load.py
@@ -40,7 +40,7 @@ def usage(error):
     print("""
     ERROR: %s
     
-    Usage: python -m pyworkflow.project.scripts.load /path/to/project
+    Usage: scipion python -m pyworkflow.project.scripts.load /path/to/project
         This script allows to quickly load a project folder without
         importing that in the general user data folder
     """ % error)
diff --git a/pyworkflow/project/scripts/refresh.py b/pyworkflow/project/scripts/refresh.py
index f3ce9d47a..b136845ad 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 scripts/refresh_project.py project_name [refresh_period]
+    Usage: scipion python -m scripts.project.refresh_project 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 scripts/refresh_project.py TestExtractParticles 15
+        scipion python -m scripts.project.refresh_project TestExtractParticles 15
     """ % error)
     sys.exit(1)
 
diff --git a/pyworkflow/project/scripts/schedule.py b/pyworkflow/project/scripts/schedule.py
index b4c9d05d4..4845c6a23 100644
--- a/pyworkflow/project/scripts/schedule.py
+++ b/pyworkflow/project/scripts/schedule.py
@@ -40,12 +40,12 @@ def usage(error):
     print("""
     ERROR: %s
     
-    Usage: python -m pyworkflow.project.scripts.schedule projectName 
+    Usage: scipion python -m pyworkflow.project.scripts.schedule projectName 
     
               options: --ignore ProtClassName1 ProtClassName2 ProtClassLabel1 ...
               
         This script will schedule all the protocols in a project. If '--ignore' 
-          options is passed, it doesn't schedule those protocols that belongs to 
+          option is passed, it doesn't schedule those protocols that belongs to 
           ProtClassName1 or ProtClassName2 class, also those protocols with a 
           objLabel equals to ProtClassLabel1
     """ % error)
@@ -93,7 +93,7 @@ def usage(error):
 
 for root in roots:
     for child in root.getChilds():
-        workflow, _ = project._getWorkflowFromProtocol(child.run)
+        workflow, _ = project._getSubworkflow(child.run)
         for prot, level in workflow.values():
             if prot.forceSchedule.get():
                 print(pwutils.blueStr("\nStopping scheduling at '%s'. Force scheduling found.\n"
diff --git a/pyworkflow/project/scripts/stack2volume.py b/pyworkflow/project/scripts/stack2volume.py
index 910e752ac..a5569567d 100755
--- a/pyworkflow/project/scripts/stack2volume.py
+++ b/pyworkflow/project/scripts/stack2volume.py
@@ -12,10 +12,10 @@ def usage(error):
     print("""
     ERROR: %s
     
-    stack2Volume will swap the dimension in the header of stack to make them 
-    volumes. Something like 10 x 1 x 10 x 10 will be change to 1 x 10 x 10 x 10
-    Usage: stack2volume.py PATH
-        PATH: path to look for stack files    
+    Usage: scipion python -m pyworkflow.project.scripts.stack2volume PATH
+        PATH: path to look for stack files
+    The script will swap the dimensions in the header of a stack to make them 
+    volumes. Something like 10 x 1 x 10 x 10 will be changed to 1 x 10 x 10 x 10
     """ % error)
     sys.exit(1)    
 
diff --git a/pyworkflow/project/scripts/stop.py b/pyworkflow/project/scripts/stop.py
index 07721299d..f7a04d816 100644
--- a/pyworkflow/project/scripts/stop.py
+++ b/pyworkflow/project/scripts/stop.py
@@ -38,10 +38,8 @@ def usage(error):
     print("""
     ERROR: %s
 
-    Usage: python -m pyworkflow.project.scripts.stop project_name
+    Usage: scipion python -m pyworkflow.project.scripts.stop project_name
         This script will stop all running protocols of the specified project.
-        e.g.
-        scipion python scripts/stop.py MyProject
     """ % error)
     sys.exit(1)
 
diff --git a/pyworkflow/protocol/executor.py b/pyworkflow/protocol/executor.py
index dd962ba19..677c39d92 100644
--- a/pyworkflow/protocol/executor.py
+++ b/pyworkflow/protocol/executor.py
@@ -284,6 +284,17 @@ def __init__(self, hostConfig, submitDict, nThreads, **kwargs):
         else:
             self.runJobs = StepExecutor.runSteps
 
+        self.renameGpuIds()
+
+    def renameGpuIds(self):
+        """ Reorganize the gpus ids starting from 0 since the queue engine is the one assigning them.
+        https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#env-vars """
+        for threadId, gpuList in self.gpuDict.items():
+            for i in range(len(gpuList)):
+                self.gpuDict[threadId][i] = i
+
+        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):
         threadId = threading.current_thread().thId
         submitDict = dict(self.hostConfig.getQueuesDefault())
@@ -316,23 +327,26 @@ def runJob(self, log, programName, params, numberOfMpi=1, numberOfThreads=1, env
         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]
+        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:
diff --git a/pyworkflow/protocol/launch.py b/pyworkflow/protocol/launch.py
index 55d52c675..eee19d29f 100644
--- a/pyworkflow/protocol/launch.py
+++ b/pyworkflow/protocol/launch.py
@@ -240,7 +240,7 @@ def analyzeFormattingTypeError(string, dictionary):
      it splits te string by \n and test the formatting per line. Raises an exception if any line fails
      with all problems found"""
 
-    # Do the replace line by line
+    # Do the replacement line by line
     lines = string.split("\n")
 
     problematicLines = []
@@ -253,8 +253,9 @@ def analyzeFormattingTypeError(string, dictionary):
             problematicLines.append(line + " --> " + str(e))
 
     if problematicLines:
-        return PyworkflowException('Following lines in %s seems to be problematic. '
-                                   'Please review its format or content.\n%s' % (pw.Config.SCIPION_HOSTS, "\n".join(problematicLines)),
+        return PyworkflowException('Following lines in %s seems to be problematic.\n'
+                                   'Values known in this context are: \n%s'
+                                   'Please review its format or content.\n%s' % (dictionary, pw.Config.SCIPION_HOSTS, "\n".join(problematicLines)),
                                    url=pw.DOCSITEURLS.HOST_CONFIG)
 
 def _submit(hostConfig, submitDict, cwd=None, env=None):
diff --git a/pyworkflow/protocol/params.py b/pyworkflow/protocol/params.py
index 404026520..d341234a8 100644
--- a/pyworkflow/protocol/params.py
+++ b/pyworkflow/protocol/params.py
@@ -436,8 +436,17 @@ def __init__(self, **args):
 
         
 class BooleanParam(Param):
-    def __init__(self, **args):
+    """ Param to store boolean values. By default it will be displayed as 2 radio buttons with Yes/no labels.
+    Alternatively, if you pass checkbox it will be displayed as a checkbox.
+
+    :param display: (Optional) default DISPLAY_YES_NO.  (Yes /no)
+                    Alternatively use BooleanParam.DISPLAY_CHECKBOX to use checkboxes """
+    DISPLAY_YES_NO = 1
+    DISPLAY_CHECKBOX = 2
+
+    def __init__(self, display=DISPLAY_YES_NO, **args):
         Param.__init__(self, paramClass=Boolean, **args)
+        self.display = display
         self.addValidator(NonEmptyBool)
 
 
diff --git a/pyworkflow/protocol/protocol.py b/pyworkflow/protocol/protocol.py
index 4544983f1..b326e0b9c 100644
--- a/pyworkflow/protocol/protocol.py
+++ b/pyworkflow/protocol/protocol.py
@@ -161,6 +161,9 @@ def getElapsedTime(self, default=dt.timedelta()):
     def setStatus(self, value):
         return self.status.set(value)
 
+    def isNew(self):
+        return self.getStatus() == STATUS_NEW
+
     def setInteractive(self, value):
         return self.interactive.set(value)
 
@@ -350,6 +353,10 @@ class MyOutput(enum.Enum):
     """
     _possibleOutputs = None
 
+    # Cache package and plugin
+    _package = None
+    _plugin = None
+
     def __init__(self, **kwargs):
         Step.__init__(self, **kwargs)
         self._size = None
@@ -489,7 +496,9 @@ def __tryUpdateOutputSet(self, outputName, outputSet,
                 outputSet.write()  # Write to commit changes
                 outputAttr = getattr(self, outputName)
                 # Copy the properties to the object contained in the protocol
-                outputAttr.copy(outputSet, copyId=False)
+                # Default Set.copy ignores some attributes like size or mapperPath.
+                # In this case we want all to be copied
+                outputAttr.copy(outputSet, copyId=False, ignoreAttrs=[])
                 # Persist changes
                 self._store(outputAttr)
             else:
@@ -538,7 +547,7 @@ def hasDefinition(cls):
         return hasattr(cls, '_definition')
 
     @classmethod
-    def isNew(cls):
+    def isNewDev(cls):
         if cls._devStatus == pw.NEW:
             return True
 
@@ -1445,6 +1454,17 @@ def cleanTmp(self):
         else:
             pwutils.cleanPath(tmpFolder)
 
+        self._cleanExtraFiles()
+    def _cleanExtraFiles(self):
+        """ This method will be called when the protocol finishes correctly.
+        It is the responsibility of the protocols to implement this method to make extra cleanup
+        of its folders, like iterations folder and files that are not needed when finished
+        """
+
+        logger.info("Nothing to clean up")
+        logger.debug('FOR DEVELOPERS: implement Protocol._cleanExtraFiles this protocol could'
+                     ' free up some space upon finishing.')
+
     def _run(self):
         # Check that a proper Steps executor have been set
         if self._stepsExecutor is None:
@@ -1504,10 +1524,12 @@ def run(self):
             self.info('Hostname: %s' % self.getHostFullName())
             self.info('PID: %s' % self.getPid())
             self.info('pyworkflow: %s' % pw.__version__)
-            self.info('plugin: %s' % self.getClassPackageName())
-            if hasattr(self.getClassPackage(), "__version__"):
-                self.info('plugin v: %s' % self.getClassPackage().__version__)
-            self.info('plugin binary v: %s' % self.getClassPackage().Plugin.getActiveVersion())
+            plugin = self.getPlugin()
+            self.info('plugin: %s' %  plugin.getName())
+            package = self.getClassPackage()
+            if hasattr(package, "__version__"):
+                self.info('plugin v: %s%s' %(package.__version__, ' (devel)' if plugin.inDevelMode() else '(production)'))
+            self.info('plugin binary v: %s' % plugin.getActiveVersion())
             self.info('currentDir: %s' % os.getcwd())
             self.info('workingDir: %s' % self.workingDir)
             self.info('runMode: %s' % MODE_CHOICES[self.runMode.get()])
@@ -1740,17 +1762,17 @@ def getClassPackage(cls):
         this method the protocol classes are registered with it Plugin
         and Domain info.
         """
-        # import pyworkflow.em as em
-        # em.Domain.getProtocols()  # make sure the _package is set for each Protocol class
-        # TODO: Check if we need to return scipion by default anymore
-        # Now the basic EM protocols are defined by scipion-em (pwem)
-        return getattr(cls, '_package', None)
+        return cls._package
 
     @classmethod
     def getClassPlugin(cls):
-        package = cls.getClassPackage()
-        return getattr(package, 'Plugin', None)
 
+        logger.warning("Deprecated on 04-2023. Use Protocol.getPlugin instead.")
+        return cls.getPlugin()
+
+    @classmethod
+    def getPlugin(cls):
+        return cls._plugin
     @classmethod
     def getClassPackageName(cls):
         return cls.getClassPackage().__name__ if cls.getClassPackage() else "orphan"
@@ -1813,7 +1835,11 @@ def getClassLabel(cls, prependPackageName=True):
         """ Return a more readable string representing the protocol class """
         label = cls.__dict__.get('_label', cls.__name__)
         if prependPackageName:
-            label = "%s - %s" % (cls.getClassPackageName(), label)
+            try:
+                label = "%s - %s" % (cls.getPlugin().getName(), label)
+            except Exception as e:
+                label = "%s -%s" % ("missing", label)
+                logger.error("Couldn't get the plugin name for %s" % label, exc_info=e)
         return label
 
     @classmethod
@@ -1943,7 +1969,7 @@ def _validate(self):
 
     @classmethod
     def getUrl(cls):
-        return cls.getClassPlugin().getUrl(cls)
+        return cls.getPlugin().getUrl(cls)
 
     @classmethod
     def isInstalled(cls):
@@ -2187,8 +2213,8 @@ def citations(self):
     def getHelpText(cls):
         """Get help text to show in the protocol help button"""
         helpText = cls.getDoc()
-        # NOt used since getClassPlugin is always None
-        # plugin = self.getClassPlugin()
+        # NOt used since getPlugin is always None
+        # plugin = self.getPlugin()
         # if plugin:
         #     pluginMetadata = plugin.metadata
         #     helpText += "\n\nPlugin info:\n"
diff --git a/pyworkflow/resources/backward-solid.png b/pyworkflow/resources/backward-solid.png
new file mode 100644
index 000000000..5cdc4f872
Binary files /dev/null and b/pyworkflow/resources/backward-solid.png differ
diff --git a/pyworkflow/resources/broom-solid.png b/pyworkflow/resources/broom-solid.png
new file mode 100644
index 000000000..897f81baa
Binary files /dev/null and b/pyworkflow/resources/broom-solid.png differ
diff --git a/pyworkflow/resources/clipboard-regular.png b/pyworkflow/resources/clipboard-regular.png
new file mode 100644
index 000000000..91dc40f82
Binary files /dev/null and b/pyworkflow/resources/clipboard-regular.png differ
diff --git a/pyworkflow/resources/code-branch-solid.png b/pyworkflow/resources/code-branch-solid.png
new file mode 100644
index 000000000..a4b7cfe59
Binary files /dev/null and b/pyworkflow/resources/code-branch-solid.png differ
diff --git a/pyworkflow/resources/file_md.png b/pyworkflow/resources/file_md.png
index 548b660af..f76928d39 100644
Binary files a/pyworkflow/resources/file_md.png and b/pyworkflow/resources/file_md.png differ
diff --git a/pyworkflow/resources/file_md_link.png b/pyworkflow/resources/file_md_link.png
new file mode 100644
index 000000000..c0943f2d6
Binary files /dev/null and b/pyworkflow/resources/file_md_link.png differ
diff --git a/pyworkflow/resources/paste-solid.png b/pyworkflow/resources/paste-solid.png
new file mode 100644
index 000000000..0ddfe12df
Binary files /dev/null and b/pyworkflow/resources/paste-solid.png differ
diff --git a/pyworkflow/resources/power-off-solid.png b/pyworkflow/resources/power-off-solid.png
new file mode 100644
index 000000000..531de7216
Binary files /dev/null and b/pyworkflow/resources/power-off-solid.png differ
diff --git a/pyworkflow/resources/scipion_logo_normal.png b/pyworkflow/resources/scipion_logo_normal.png
index 931ac7bff..d559a7bfb 100644
Binary files a/pyworkflow/resources/scipion_logo_normal.png and b/pyworkflow/resources/scipion_logo_normal.png differ
diff --git a/pyworkflow/resources/scipion_logo_small.png b/pyworkflow/resources/scipion_logo_small.png
index 7b8919b03..6a3fe5c3b 100644
Binary files a/pyworkflow/resources/scipion_logo_small.png and b/pyworkflow/resources/scipion_logo_small.png differ
diff --git a/pyworkflow/template.py b/pyworkflow/template.py
index f0f7f32ca..e55ea1e28 100644
--- a/pyworkflow/template.py
+++ b/pyworkflow/template.py
@@ -261,7 +261,7 @@ def addPluginTemplates(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)
         for pluginName, pluginModule in domain.getPlugins().items():
-            tempListPlugin = pluginModule.Plugin.getTemplates()
+            tempListPlugin = pluginModule._pluginInstance.getTemplates()
             for t in tempListPlugin:
                 if tempId is not None:
                     if t.getObjId() == tempId:
diff --git a/pyworkflow/utils/graph.py b/pyworkflow/utils/graph.py
index 660e8ed03..f9de517a6 100644
--- a/pyworkflow/utils/graph.py
+++ b/pyworkflow/utils/graph.py
@@ -153,6 +153,10 @@ def aliasNode(self, node, aliasName):
     def getNode(self, nodeName):
         return self._nodesDict.get(nodeName, None)
 
+    def getNodeNames(self):
+        """ Returns all the keys in the node dictionary"""
+        return self._nodesDict.keys()
+
     def getNodes(self):
         return self._nodes
 
diff --git a/pyworkflow/utils/log.py b/pyworkflow/utils/log.py
index 204f3f31b..d1a2d55a9 100644
--- a/pyworkflow/utils/log.py
+++ b/pyworkflow/utils/log.py
@@ -147,7 +147,7 @@ def setupDefaultLogging(logFile=None, console=True):
 
         if console:
             config["handlers"][CONSOLE_HANDLER] = {
-                        'level': Config.SCIPION_LOG_LEVEL,
+                        'level': 'ERROR', #Config.SCIPION_LOG_LEVEL,
                         'class': 'logging.StreamHandler',
                         'formatter': 'standard',
                         }
diff --git a/pyworkflow/utils/path.py b/pyworkflow/utils/path.py
index 70cd8a948..165de88b7 100644
--- a/pyworkflow/utils/path.py
+++ b/pyworkflow/utils/path.py
@@ -33,7 +33,7 @@
 from glob import glob
 import datetime
 
-from pyworkflow import SCIPION_SCRATCH, DOCSITEURLS
+from pyworkflow import SCIPION_SCRATCH, DOCSITEURLS, ASCII_COLOR_2_TKINTER
 from pyworkflow.exceptions import PyworkflowException
 from pyworkflow.config import Config
 
@@ -312,18 +312,6 @@ def commonPath(*paths):
     return os.path.dirname(os.path.commonprefix(*paths))
 
 
-# Console (and XMIPP) escaped colors, and the related tags that we create
-# with Text.tag_config(). This dict is used in OutputText:addLine()
-# See also http://www.termsys.demon.co.uk/vtansi.htm#colors
-colorName = {'30': 'gray',
-             '31': 'red',
-             '32': 'green',
-             '33': 'yellow',
-             '34': 'blue',
-             '35': 'magenta',
-             '36': 'cyan',
-             '37': 'white'}
-
 
 def renderTextFile(fname, add, offset=0, lineNo=0, numberLines=True,
                    maxSize=400, headSize=40, tailSize=None, notifyLine=None, errors='strict'):
@@ -400,7 +388,7 @@ def renderLine(line, add, lineNo=1, numberLines=True):
         if code == '0':
             attribute = None
         else:
-            attribute = colorName.get(code[-2:], None)
+            attribute = ASCII_COLOR_2_TKINTER.get(code[-2:], None)
         pos = end + 1  # go to the character next to "m", the closing char
 
 
diff --git a/pyworkflow/utils/process.py b/pyworkflow/utils/process.py
index 92d21d5a0..1ffae0f7a 100644
--- a/pyworkflow/utils/process.py
+++ b/pyworkflow/utils/process.py
@@ -48,9 +48,10 @@ def runJob(log, programname, params,
                               env, gpuList=gpuList)
     
     if log is None:
-        logger.info("** Running command: %s" % greenStr(command))
-    else:
-        log.info(greenStr(command))
+        log = logger
+
+    log.info("** Running command: **")
+    log.info(greenStr(command))
 
     return runCommand(command, env, cwd)
         
diff --git a/pyworkflow/utils/properties.py b/pyworkflow/utils/properties.py
index 17bec0ba1..0ec489960 100644
--- a/pyworkflow/utils/properties.py
+++ b/pyworkflow/utils/properties.py
@@ -28,7 +28,8 @@
 """
 This module defines the text used in the application.
 """
-
+# NOTE: DO NOT REMOVE UNTIL plugin manager uses Config.SCIPION_MAIN_COLOR and is released
+from pyworkflow.constants import Color
 
 class Message:
     # Example Usage: 
@@ -68,24 +69,20 @@ class Message:
     LABEL_NEW_ACTION = 'New     '
     LABEL_EDIT = 'Edit'
     LABEL_RENAME = 'Rename '
-    LABEL_EDIT_ACTION = 'Edit     '
     LABEL_COPY = 'Copy'
-    LABEL_COPY_ACTION = 'Copy   '
+    LABEL_PASTE = 'Paste'
+    LABEL_DUPLICATE = 'Duplicate'
     LABEL_DELETE = 'Delete'
-    LABEL_DELETE_ACTION = 'Delete    '
     LABEL_STEPS = 'Steps'
     LABEL_BROWSE = 'Browse'
-    LABEL_BROWSE_ACTION = 'Browse '
     LABEL_DB = 'Db'
     LABEL_STOP = 'Stop'
-    LABEL_STOP_ACTION = 'Stop execution'
     LABEL_ANALYZE = 'Analyze Results'
     LABEL_TREE = 'Tree'
     LABEL_SMALLTREE = 'Small Tree'
     LABEL_REFRESH = 'Refresh'
     LABEL_DEFAULT = 'Default'
     LABEL_CONTINUE = 'Continue'
-    LABEL_CONTINUE_ACTION = 'Approve continue'
     LABEL_EXPORT = 'Export'
     LABEL_EXPORT_UPLOAD = 'Export & upload'
     LABEL_RESTART_WORKFLOW = 'Restart workflow'
@@ -277,6 +274,7 @@ class Message:
     LABEL_SAVED_FORM = 'The protocol was saved successfully.'
     TITLE_DELETE_FORM = 'Confirm DELETE'
     TITLE_RESTART_FORM = 'Confirm RESTART'
+    TITLE_CONTINUE_FORM = 'Confirm CONTINUE'
     LABEL_DELETE_FORM = """
 You are going to *DELETE* the run(s): 
   - %s
@@ -284,21 +282,40 @@ class Message:
 
 Do you really want to continue?'
 """
-    LABEL_RESTART_FORM = """
+    MESSAGE_RESTART_FORM = """
 You are going to *RESTART* the run: 
   %s
 
-Do you really want to restart it?'
+Do you really want to restart it?
 """
-    LABEL_RESTART_FORM_MANY = """
-Following data will be *REMOVED* : 
-  %s
+    MESSAGE_CONTINUE_FORM = """
+    You are going to *CONTINUE* the run: 
+      %s
 
-What do you want to restart?
+    Do you really want to continue it?
+    """
+
+    MESSAGE_RESTART_WORKFLOW_WITH_RESULTS = """
+    All previous results of the following protocols will be deleted:
+        %s
+
+        Do you really want to *RESTART* the workflow?'
+    """
+
+    MESSAGE_CONTINUE_WORKFLOW_WITH_RESULTS = """
+        All previous results of the following protocols will be affected: 
+        %s
+
+        Do you really want to *CONTINUE* the workflow?
+        """
+
+    MESSAGE_ASK_SINGLE_ALL = """
+        What do you want to do?
+
+        *Single* : Just this protocol.
+        *All*: All above listed protocols will be launched.
+    """
 
-  *Single* : Just this protocol, rest will be reset.
-  *All*: All above listed protocols will be restarted.
-"""
 
     TITLE_STOP_FORM = 'Confirm STOP'
     LABEL_STOP_FORM = 'Do you really want to *STOP* this run?'
@@ -308,10 +325,12 @@ class Message:
     TITLE_SAVE_OUTPUT = 'Save protocol output'
     LABEL_SAVE_OUTPUT = 'Do you wish to save protocol output?'
 
-    TITLE_RESTART_WORKFLOW_FORM = 'Confirm RESTART'
+    TITLE_RESTART_WORKFLOW_FORM = 'Confirm RESTART workflow'
+    TITLE_CONTINUE_WORKFLOW_FORM = 'Confirm CONTINUE workflow'
     TITLE_STOP_WORKFLOW_FORM = 'Confirm STOP'
     TITLE_RESET_WORKFLOW_FORM = 'Confirm RESET'
-    TITLE_RESTART_WORKFLOW = 'Do you really want to *RESTART* this Workflow? All previous results will be deleted'
+    MESSAGE_RESTART_WORKFLOW = 'Do you really want to *RESTART* this workflow?'
+    MESSAGE_CONTINUE_WORKFLOW = 'Do you really want to *CONTINUE* this workflow?'
     TITLE_STOP_WORKFLOW = 'Do you really want to *STOP* this Workflow?'
     TITLE_RESET_WORKFLOW = 'Do you really want to *RESET* this Workflow?'
     TITLE_LAUNCHED_WORKFLOW_FAILED_FORM = 'Error while launching the workflow'
@@ -432,11 +451,12 @@ class Icon:
     ACTION_EDIT = 'fa-pencil.png'
     ACTION_SELECT_FROM = 'fa-arrow-down.png'
     ACTION_SELECT_TO = 'fa-arrow-up.png'
-    ACTION_COPY = 'fa-files-o.png'
+    ACTION_COPY = 'clipboard-regular.png'
+    ACTION_PASTE = 'paste-solid.png'
+    ACTION_DUPLICATE = 'fa-files-o.png'
     ACTION_DELETE = 'fa-trash-o.png'
     ACTION_REFRESH = 'fa-refresh.png'
     ACTION_RENAME = 'rename.png'
-    # TODO: change action_steps icon - fa-codefork?
     ACTION_STEPS = 'fa-list-ul.png'
     ACTION_BROWSE = 'fa-folder-open.png'
     ACTION_DB = 'fa-database.png'
@@ -481,6 +501,10 @@ class Icon:
     FOLDER_OPEN = 'fa-folder-open.png'
     DB = 'file_sqlite.png'
     TXT_FILE = 'file_text.png'
+    POWER_OFF = 'power-off-solid.png'
+    BROOM = 'broom-solid.png'
+    BACKWARD = 'backward-solid.png'
+    CODE_BRANCH = 'code-branch-solid.png'
 
     SCIPION_ICON = 'scipion_icon.png'
     SCIPION_ICON_PROJ = SCIPION_ICON  # 'scipion_icon_proj.png'
@@ -508,21 +532,6 @@ class Icon:
     PLUGIN_PACKAGE = 'file_folder.png'
 
 
-class Color:
-    RED_COLOR = 'Firebrick'  # Red color for background label  = #B22222
-    LIGHT_RED_COLOR = '#F3CBCB'  # Very light red for row selection
-    LIGHT_BLUE_COLOR = '#EAEBFF'  # Very light blue for even rows
-    LIGHT_GREY_COLOR = '#EAEBEC'  # Light grey for background color in form, protocol, table header and west container
-    LIGHT_GREY_COLOR_2 = '#F2F2F2'  # Very light grey for odd rows, input background, etc
-    DARK_GREY_COLOR = '#6E6E6E'  # Very dark grey for project title, tubes, etc
-    
-    STATUS_SAVED = '#D9F1FA', 
-    STATUS_LAUNCHED = '#D9F1FA', 
-    STATUS_RUNNING = '#FCCE62', 
-    STATUS_FINISHED = '#D2F5CB', 
-    STATUS_FAILED = '#F5CCCB', 
-    STATUS_INTERACTIVE = '#F3F5CB',
-    STATUS_ABORTED = '#F5CCCB',
 
 
 class colorText:
diff --git a/pyworkflow/utils/reflection.py b/pyworkflow/utils/reflection.py
index f3b37b9fe..010b06eac 100644
--- a/pyworkflow/utils/reflection.py
+++ b/pyworkflow/utils/reflection.py
@@ -95,7 +95,8 @@ def getSubclasses(BaseClass, inputDict):
     """
     outputDict = {}
     for k, v in inputDict.items():
-        if isclass(v) and issubclass(v, BaseClass):
+        # Do not add the base class that is imported in all modules
+        if (not v==BaseClass) and isclass(v) and issubclass(v, BaseClass):
             outputDict[k] = v
     return outputDict
 
diff --git a/pyworkflow/utils/utils.py b/pyworkflow/utils/utils.py
index 009cec10f..e2eb856f8 100644
--- a/pyworkflow/utils/utils.py
+++ b/pyworkflow/utils/utils.py
@@ -30,13 +30,13 @@
 import re
 from datetime import datetime
 import traceback
-from enum import Enum
+import sysconfig
 
 import bibtexparser
 import numpy as np
 import math
 
-from pyworkflow import Config
+from pyworkflow import Config, StrColors
 
 
 def prettyDate(time=False):
@@ -348,6 +348,16 @@ def getHostFullName():
     import socket
     return socket.getfqdn()
 
+def getPython():
+    return sys.executable
+
+def getPythonPackagesFolder():
+    # This does not work on MAC virtual envs
+    # import site
+    # return site.getsitepackages()[0]
+
+    return sysconfig.get_path("platlib")
+
 
 # ******************************File utils *******************************
 
@@ -396,15 +406,6 @@ def hasFileChangedSince(file, time):
 
 # ------------- Colored message strings -----------------------------
 
-class StrColors(Enum):
-    gray = 30
-    red = 31
-    green = 32
-    yellow = 33
-    blue = 34
-    magenta = 35
-    cyan = 36
-
 
 def getColorStr(text, color, bold=False):
     """ Add ANSI color codes to the string if there is a terminal sys.stdout.
diff --git a/pyworkflow/viewer.py b/pyworkflow/viewer.py
index 29a6f1f63..9c294e541 100644
--- a/pyworkflow/viewer.py
+++ b/pyworkflow/viewer.py
@@ -121,6 +121,7 @@ class Viewer(object):
     """
     _targets = []
     _environments = [DESKTOP_TKINTER]
+    _name = None
     
     def __init__(self, tmpPath='./Tmp', **args):
         self._tmpPath = tmpPath
@@ -288,6 +289,9 @@ def _viewAll(self, *args):
     def _citations(self):
         return self.protocol._citations()
 
+    def validateInstallation(self):
+        return
+
     # TODO deprecate this method, it's duplicate of one from pwutils.utils
     def _getListFromRangeString(self, rangeStr):
         """ Create a list of integer from a string with range definitions


Test patch:
diff --git a/pyworkflow/apps/pw_run_tests.py b/pyworkflow/apps/pw_run_tests.py
index c26bb5866..a8b156b1b 100644
--- a/pyworkflow/apps/pw_run_tests.py
+++ b/pyworkflow/apps/pw_run_tests.py
@@ -110,6 +110,7 @@ def main(self, args=None):
             for k, p in self.paths:
                 testPath = os.path.join(p, k, 'tests')
                 if os.path.exists(testPath):
+                    self.logger.debug("Discovering tests at %s" % testPath)
                     testsDict[k] = testLoader.discover(testPath,
                                                        pattern=args.pattern,
                                                        top_level_dir=p)
diff --git a/pyworkflow/tests/tests.py b/pyworkflow/tests/tests.py
index 457f5fbbe..6dd639660 100644
--- a/pyworkflow/tests/tests.py
+++ b/pyworkflow/tests/tests.py
@@ -12,6 +12,7 @@
 import pyworkflow.utils as pwutils
 from pyworkflow.project import Manager
 from pyworkflow.protocol import MODE_RESTART, getProtocolFromDb
+from pyworkflow.object import Set
 
 SMALL = 'small'
 PULL_REQUEST = 'pull'
@@ -211,6 +212,12 @@ def compareSets(cls, test, set1, set2):
                 item2.printAll()
             test.assertTrue(areEqual)
 
+    def compareSetProperties(self, set1:Set, set2:Set, ignore = ["_size", "_mapperPath"]):
+        """ Compares 2 sets' properties"""
+
+        self.assertTrue(set1.equalAttributes(set2, ignore=ignore, verbose=True), "Set1 (%s) properties differs from set2 (%s)." % (set1, set2))
+        self.assertTrue(set2.equalAttributes(set1, ignore=ignore, verbose=True), 'Set2 (%s) has properties that set1 (%s) does not have.' % (set2, set1))
+
     def assertSetSize(self, setObject, size=None, msg=None, diffDelta=None):
         """ Check if a pyworkflow Set is not None nor is empty, or of a determined size or
         of a determined size with a percentage (base 1) of difference"""
diff --git a/pyworkflowtests/__init__.py b/pyworkflowtests/__init__.py
index 552674a20..1defb9142 100644
--- a/pyworkflowtests/__init__.py
+++ b/pyworkflowtests/__init__.py
@@ -29,6 +29,7 @@
 from pyworkflow.protocol import Protocol
 from pyworkflow.wizard import Wizard
 from pyworkflow.viewer import Viewer
+from pyworkflow.utils import getSubclasses
 from pyworkflow.plugin import Domain as pwDomain, Plugin as pwPlugin
 
 from .objects import MockObject
@@ -40,7 +41,7 @@ class TestDomain(pwDomain):
     _protocolClass = Protocol
     _viewerClass = Viewer
     _wizardClass = Wizard
-    _baseClasses = globals()
+    _baseClasses = getSubclasses(MockObject, globals())
 
 
 Domain = TestDomain
diff --git a/pyworkflowtests/objects.py b/pyworkflowtests/objects.py
index b89de46f2..6a37a893a 100644
--- a/pyworkflowtests/objects.py
+++ b/pyworkflowtests/objects.py
@@ -27,6 +27,8 @@
 """
 
 import os
+import logging
+logger = logging.getLogger(__name__)
 
 import pyworkflow.object as pwobj
 from pyworkflow.utils import cleanPath
@@ -614,7 +616,7 @@ def __str__(self):
         sampling = self.getSamplingRate()
 
         if not sampling:
-            print("FATAL ERROR: Object %s has no sampling rate!!!"
+            logger.info("FATAL ERROR: Object %s has no sampling rate!!!"
                   % self.getName())
             sampling = -999.0
 
diff --git a/pyworkflowtests/tests/test_object.py b/pyworkflowtests/tests/test_object.py
index b497dedc6..e33855eb8 100755
--- a/pyworkflowtests/tests/test_object.py
+++ b/pyworkflowtests/tests/test_object.py
@@ -56,7 +56,7 @@ def setUpClass(cls):
     def test_ObjectsDict(self):
         # Validate that the object dict is populated correctly
         basicObjNames = [
-            'Object', 'Scalar', 'Integer', 'Float', 'String', 'Pointer',
+            'Scalar', 'Integer', 'Float', 'String', 'Pointer', 'Boolean',
             'OrderedObject', 'List', 'CsvList', 'PointerList', 'Set'
         ]
         self.assertTrue(all(name in pwobj.OBJECTS_DICT
@@ -346,6 +346,10 @@ def checkSetIteration(limit, skipRows=None):
         imgSetVerbose = MockSetOfImages(filename=fn)
         imgSetVerbose.loadAllProperties()
 
+        # Compare sets are "equal"
+        self.compareSetProperties(imgSet, imgSetVerbose, ignore=[])
+
+
     def test_copyAttributes(self):
         """ Check that after copyAttributes, the values
         were properly copied.