Beispiel #1
0
    def contextMenuEvent(self, event):
        """contextMenuEvent(event)
        Show the context menu.
        """

        QtWidgets.QTreeView.contextMenuEvent(self, event)

        # Get if an item is selected
        item = self.currentItem()
        if not item:
            return

        # Create menu
        self._menu.clear()
        commands = [
            ("Show namespace", pyzo.translate("pyzoWorkspace",
                                              "Show namespace")),
            ("Show help", pyzo.translate("pyzoWorkspace", "Show help")),
            ("Delete", pyzo.translate("pyzoWorkspace", "Delete")),
        ]
        for a, display in commands:
            action = self._menu.addAction(display)
            action._what = a
            parts = splitName(self._proxy._name)
            parts.append(item.text(0))
            action._objectName = joinName(parts)
            action._item = item

        # Show
        self._menu.popup(QtGui.QCursor.pos() + QtCore.QPoint(3, 3))
Beispiel #2
0
class FakeEditor(pyzo.core.baseTextCtrl.BaseTextCtrl):
    """This "fake" editor emits a signal when
    the user clicks on a word with a token:
    a click on the word "class" emits with arg "syntax.keyword".

    It may be improved by adding text with specific token
    like Editor.text which are not present by default
    """

    tokenClicked = QtCore.Signal(str)

    def __init__(self, text=""):
        super().__init__()

        # set parser to enable syntaxic coloration
        self.setParser("python3")
        self.setReadOnly(False)
        self.setLongLineIndicatorPosition(30)
        self.setPlainText(SAMPLE)

    def mousePressEvent(self, event):
        super().mousePressEvent(event)

        # get the text position of the click
        pos = self.textCursor().columnNumber()
        tokens = self.textCursor().block().userData().tokens

        # Find the token which contains the click pos
        for tok in tokens:
            if tok.start <= pos <= tok.end:
                self.tokenClicked.emit(tok.description.key)
                break
Beispiel #3
0
    def __init__(self):
        QtWidgets.QToolButton.__init__(self)

        # Init
        self.setIconSize(QtCore.QSize(*self.SIZE))
        self.setStyleSheet("QToolButton{ border:none; padding:0px; margin:0px; }")
        self.setIcon(self.getCrossIcon1())
Beispiel #4
0
def loadIcons():
    """loadIcons()
    Load all icons in the icon dir.
    """
    # Get directory containing the icons
    iconDir = os.path.join(pyzo.pyzoDir, "resources", "icons")

    # Construct other icons
    dummyIcon = IconArtist().finish()
    pyzo.icons = ssdf.new()
    for fname in os.listdir(iconDir):
        if fname.endswith(".png"):
            try:
                # Short and full name
                name = fname.split(".")[0]
                name = name.replace("pyzo_", "")  # discart prefix
                ffname = os.path.join(iconDir, fname)
                # Create icon
                icon = QtGui.QIcon()
                icon.addFile(ffname, QtCore.QSize(16, 16))
                # Store
                pyzo.icons[name] = icon
            except Exception as err:
                pyzo.icons[name] = dummyIcon
                print("Could not load icon %s: %s" % (fname, str(err)))
Beispiel #5
0
def loadAppIcons():
    """loadAppIcons()
    Load the application iconsr.
    """
    # Get directory containing the icons
    appiconDir = os.path.join(pyzo.pyzoDir, "resources", "appicons")

    # Determine template for filename of the application icon-files.
    fnameT = "pyzologo{}.png"

    # Construct application icon. Include a range of resolutions. Note that
    # Qt somehow does not use the highest possible res on Linux/Gnome(?), even
    # the logo of qt-designer when alt-tabbing looks a bit ugly.
    pyzo.icon = QtGui.QIcon()
    for sze in [16, 32, 48, 64, 128, 256]:
        fname = os.path.join(appiconDir, fnameT.format(sze))
        if os.path.isfile(fname):
            pyzo.icon.addFile(fname, QtCore.QSize(sze, sze))

    # Set as application icon. This one is used as the default for all
    # windows of the application.
    QtWidgets.qApp.setWindowIcon(pyzo.icon)

    # Construct another icon to show when the current shell is busy
    artist = IconArtist(pyzo.icon)  # extracts the 16x16 version
    artist.setPenColor("#0B0")
    for x in range(11, 16):
        d = x - 11  # runs from 0 to 4
        artist.addLine(x, 6 + d, x, 15 - d)
    pm = artist.finish().pixmap(16, 16)
    #
    pyzo.iconRunning = QtGui.QIcon(pyzo.icon)
    pyzo.iconRunning.addPixmap(pm)  # Change only 16x16 icon
Beispiel #6
0
 def onSearchFinish(self, hits):
     if hits == 0:
         return
     hits = self._searchEngine.hits(0, hits)
     if not hits:
         return
     url = self.find_best_page(hits)
     self._helpBrowser.setSource(QtCore.QUrl(url))
Beispiel #7
0
    def __init__(self, parent, **kwds):
        super().__init__(parent, showLineNumbers=True, **kwds)

        # Init filename and name
        self._filename = ""
        self._name = "<TMP>"

        # View settings
        # TODO: self.setViewWrapSymbols(view.showWrapSymbols)
        self.setShowLineEndings(pyzo.config.view.showLineEndings)
        self.setShowIndentationGuides(pyzo.config.view.showIndentationGuides)
        #
        self.setWrap(bool(pyzo.config.view.wrap))
        self.setHighlightCurrentLine(pyzo.config.view.highlightCurrentLine)
        self.setLongLineIndicatorPosition(pyzo.config.view.edgeColumn)
        # TODO: self.setFolding( int(view.codeFolding)*5 )
        # bracematch is set in baseTextCtrl, since it also applies to shells
        # dito for zoom and tabWidth

        # Set line endings to default
        self.lineEndings = pyzo.config.settings.defaultLineEndings

        # Set encoding to default
        self.encoding = "UTF-8"

        # Modification time to test file change
        self._modifyTime = 0

        self.modificationChanged.connect(self._onModificationChanged)

        # To see whether the doc has changed to update the parser.
        self.textChanged.connect(self._onModified)

        # This timer is used to hide the marker that shows which code is executed
        self._showRunCursorTimer = QtCore.QTimer()

        # Add context menu (the offset is to prevent accidental auto-clicking)
        self._menu = EditorContextMenu(self)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(lambda p: self._menu.popup(
            self.mapToGlobal(p) + QtCore.QPoint(0, 3)))

        # Update status bar
        self.cursorPositionChanged.connect(self._updateStatusBar)
Beispiel #8
0
    def __init__(self, *args, padding=(4, 4, 6, 6), preventEqualTexts=True):
        QtWidgets.QTabBar.__init__(self, *args)

        # Put tab widget in document mode
        self.setDocumentMode(True)

        # Widget needs to draw its background (otherwise Mac has a dark bg)
        self.setDrawBase(False)
        if sys.platform == "darwin":
            self.setAutoFillBackground(True)

        # Set whether we want to prevent eliding for names that start the same.
        self._preventEqualTexts = preventEqualTexts

        # Allow moving tabs around
        self.setMovable(True)

        # Get padding
        if isinstance(padding, (int, float)):
            padding = padding, padding, padding, padding
        elif isinstance(padding, (tuple, list)):
            pass
        else:
            raise ValueError("Invalid value for padding.")

        # Set style sheet
        stylesheet = STYLESHEET
        stylesheet = stylesheet.replace("PADDING_TOP", str(padding[0]))
        stylesheet = stylesheet.replace("PADDING_BOTTOM", str(padding[1]))
        stylesheet = stylesheet.replace("PADDING_LEFT", str(padding[2]))
        stylesheet = stylesheet.replace("PADDING_RIGHT", str(padding[3]))
        self.setStyleSheet(stylesheet)

        # We do our own eliding
        self.setElideMode(QtCore.Qt.ElideNone)

        # Make tabs wider if there's plenty space?
        self.setExpanding(False)

        # If there's not enough space, use scroll buttons
        self.setUsesScrollButtons(True)

        # When a tab is removed, select previous
        self.setSelectionBehaviorOnRemove(self.SelectPreviousTab)

        # Init alignment parameters
        self._alignWidth = MIN_NAME_WIDTH  # Width in characters
        self._alignWidthIsReducing = False  # Whether in process of reducing

        # Create timer for aligning
        self._alignTimer = QtCore.QTimer(self)
        self._alignTimer.setInterval(10)
        self._alignTimer.setSingleShot(True)
        self._alignTimer.timeout.connect(self._alignRecursive)
Beispiel #9
0
def start():
    """Run Pyzo."""

    # Do some imports
    import pyzo
    from pyzo.core import pyzoLogging  # noqa - to start logging asap
    from pyzo.core.main import MainWindow

    # Apply users' preferences w.r.t. date representation etc
    # this is required for e.g. strftime("%c")
    # Just using '' does not seem to work on OSX. Thus
    # this odd loop.
    # locale.setlocale(locale.LC_ALL, "")
    for x in ("", "C", "en_US", "en_US.utf8", "en_US.UTF-8"):
        try:
            locale.setlocale(locale.LC_ALL, x)
            break
        except Exception:
            pass

    # # Set to be aware of the systems native colors, fonts, etc.
    # QtWidgets.QApplication.setDesktopSettingsAware(True)

    # Instantiate the application
    QtWidgets.qApp = MyApp(sys.argv)  # QtWidgets.QApplication([])

    # Choose language, get locale
    appLocale = setLanguage(pyzo.config.settings.language)

    # Create main window, using the selected locale
    MainWindow(None, appLocale)

    # In test mode, we close after 5 seconds
    # We also write "Closed" to the log (if a filename is provided) which we use
    # in our tests to determine that Pyzo did a successful run.
    if "--test" in sys.argv:
        close_signal = lambda: print("Stopping")
        if os.getenv("PYZO_LOG", ""):
            close_signal = lambda: open(os.getenv("PYZO_LOG"), "at").write(
                "Stopping\n")
        pyzo.test_close_timer = t = QtCore.QTimer()
        t.setInterval(5000)
        t.setSingleShot(True)
        t.timeout.connect(lambda: [close_signal(), pyzo.main.close()])
        t.start()

    # Enter the main loop
    if hasattr(QtWidgets.qApp, "exec"):
        QtWidgets.qApp.exec()
    else:
        QtWidgets.qApp.exec_()
Beispiel #10
0
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

        # Set style/theme
        try:
            theme = pyzo.themes[pyzo.config.settings.theme.lower()]["data"]
            self.setStyle(theme)
            # autocomplete popup theme
            if pyzo.config.view.get("autoComplete_withTheme", False):
                editor_text_theme = theme["editor.text"].split(",")
                popup_background = editor_text_theme[1].split(":")[1]
                popup_text = editor_text_theme[0].split(":")[1]
                autoComplete_theme = "color: {}; background-color:{};".format(
                    popup_text, popup_background
                )
                self.completer().popup().setStyleSheet(autoComplete_theme)
        except Exception as err:
            print("Could not load theme: " + str(err))

        # Set font and zooming
        self.setFont(pyzo.config.view.fontname)
        self.setZoom(pyzo.config.view.zoom)
        self.setShowWhitespace(pyzo.config.view.showWhitespace)
        self.setHighlightMatchingBracket(pyzo.config.view.highlightMatchingBracket)

        # Create timer for autocompletion delay
        self._delayTimer = QtCore.QTimer(self)
        self._delayTimer.setSingleShot(True)
        self._delayTimer.timeout.connect(self._introspectNow)

        # For buffering autocompletion and calltip info
        self._callTipBuffer_name = ""
        self._callTipBuffer_time = 0
        self._callTipBuffer_result = ""
        self._autoCompBuffer_name = ""
        self._autoCompBuffer_time = 0
        self._autoCompBuffer_result = []

        self.setAutoCompletionAcceptKeysFromStr(
            pyzo.config.settings.autoComplete_acceptKeys
        )

        self.completer().highlighted.connect(self.updateHelp)
        self.setIndentUsingSpaces(pyzo.config.settings.defaultIndentUsingSpaces)
        self.setIndentWidth(pyzo.config.settings.defaultIndentWidth)
        self.setAutocompletPopupSize(*pyzo.config.view.autoComplete_popupSize)
        self.setAutocompleteMinChars(pyzo.config.settings.autoComplete_minChars)
        self.setCancelCallback(self.restoreHelp)
Beispiel #11
0
def setLanguage(languageName):
    """setLanguage(languageName)
    Set the language for the app. Loads qt and pyzo translations.
    Returns the QLocale instance to pass to the main widget.
    """

    # Get locale
    locale = getLocale(languageName)

    # Get paths were language files are
    qtTransPath = str(
        QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath))
    pyzoTransPath = os.path.join(pyzo.pyzoDir, "resources", "translations")

    # Get possible names for language files
    # (because Qt's .tr files may not have the language component.)
    localeName1 = locale.name()
    localeName2 = localeName1.split("_")[0]

    # Uninstall translators
    if not hasattr(QtCore, "_translators"):
        QtCore._translators = []
    for trans in QtCore._translators:
        QtWidgets.QApplication.removeTranslator(trans)

    # The default language
    if localeName1 == "C":
        return locale

    # Set Qt translations
    # Note that the translator instances must be stored
    # Note that the load() method is very forgiving with the file name
    for what, where in [("qt", qtTransPath), ("pyzo", pyzoTransPath)]:
        trans = QtCore.QTranslator()
        # Try loading both names
        for localeName in [localeName1, localeName2]:
            success = trans.load(what + "_" + localeName + ".tr", where)
            if success:
                QtWidgets.QApplication.installTranslator(trans)
                QtCore._translators.append(trans)
                print("loading %s %s: ok" % (what, languageName))
                break
        else:
            print("loading %s %s: failed" % (what, languageName))

    # Done
    return locale
Beispiel #12
0
    def __init__(self):
        QtWidgets.QToolButton.__init__(self)

        # Init
        self.setIconSize(QtCore.QSize(*self.SIZE))
        self.setStyleSheet("QToolButton{ border: none; }")

        # Create arrow pixmaps
        self._menuarrow1 = self._createMenuArrowPixmap(0)
        self._menuarrow2 = self._createMenuArrowPixmap(70)
        self._menuarrow = self._menuarrow1

        # Variable to keep icon
        self._icon = None

        # Variable to keep track of when the mouse was pressed, so that
        # we can allow dragging as well as clicking the menu.
        self._menuPressed = False
Beispiel #13
0
    def __init__(self, objectWithIcon):

        self._objectWithIcon = objectWithIcon

        # Motion properties
        self._index = 0
        self._level = 0
        self._count = 0  #  to count number of iters in level 1

        # Prepare blob pixmap
        self._blob = self._createBlobPixmap()
        self._legs = self._createLegsPixmap()

        # Create timer
        self._timer = QtCore.QTimer(None)
        self._timer.setInterval(200)
        self._timer.setSingleShot(False)
        self._timer.timeout.connect(self.onTimer)
Beispiel #14
0
    def __init__(self, engine):
        super().__init__()
        self._engine = engine
        layout = QtWidgets.QVBoxLayout(self)
        add_button = QtWidgets.QPushButton("Add")
        del_button = QtWidgets.QPushButton("Delete")
        self._view = QtWidgets.QListView()
        layout.addWidget(self._view)
        layout2 = QtWidgets.QHBoxLayout()
        layout2.addWidget(add_button)
        layout2.addWidget(del_button)
        layout.addLayout(layout2)
        self._model = QtCore.QStringListModel()
        self._view.setModel(self._model)

        self._model.setStringList(self._engine.registeredDocumentations())

        add_button.clicked.connect(self.add_doc)
        del_button.clicked.connect(self.del_doc)
Beispiel #15
0
 def helpOnText(self, pos):
     hw = pyzo.toolManager.getTool("pyzointeractivehelp")
     if not hw:
         return
     name = self.textCursor().selectedText().strip()
     if name == "":
         cursor = self.cursorForPosition(pos - self.mapToGlobal(QtCore.QPoint(0, 0)))
         line = cursor.block().text()
         limit = cursor.positionInBlock()
         while limit < len(line) and (
             line[limit].isalnum() or line[limit] in (".", "_")
         ):
             limit += 1
             cursor.movePosition(cursor.Right)
         _, tokens = self.getTokensUpToCursor(cursor)
         nameBefore, name = parseLine_autocomplete(tokens)
         if nameBefore:
             name = "%s.%s" % (nameBefore, name)
     if name != "":
         hw.setObjectName(name, True)
Beispiel #16
0
    def __init__(self, parent):
        QtWidgets.QWidget.__init__(self, parent)

        # create toolbar
        self._toolbar = QtWidgets.QToolBar(self)
        self._toolbar.setMaximumHeight(26)
        self._toolbar.setIconSize(QtCore.QSize(16, 16))

        # create stack
        self._stack = QtWidgets.QStackedWidget(self)

        # Populate toolbar
        self._shellButton = ShellControl(self._toolbar, self._stack)
        self._debugmode = 0
        self._dbs = DebugStack(self._toolbar)
        #
        self._toolbar.addWidget(self._shellButton)
        self._toolbar.addSeparator()
        # self._toolbar.addWidget(self._dbc) -> delayed, see addContextMenu()

        self._interpreterhelp = InterpreterHelper(self)

        # widget layout
        layout = QtWidgets.QVBoxLayout()
        layout.setSpacing(0)
        # set margins
        margin = pyzo.config.view.widgetMargin
        layout.setContentsMargins(margin, margin, margin, margin)

        layout.addWidget(self._toolbar)
        layout.addWidget(self._stack, 0)
        layout.addWidget(self._interpreterhelp, 0)
        self.setLayout(layout)

        # make callbacks
        self._stack.currentChanged.connect(self.onCurrentChanged)

        self.showInterpreterHelper()
Beispiel #17
0
    def __init__(self, parent, shellStack):
        QtWidgets.QToolButton.__init__(self, parent)

        # Store reference of shell stack
        self._shellStack = shellStack

        # Keep reference of actions corresponding to shells
        self._shellActions = []

        # Set text and tooltip
        self.setText("Warming up ...")
        self.setToolTip(translate("shells", "Click to select shell."))
        self.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
        self.setPopupMode(self.InstantPopup)

        # Set icon
        self._iconMaker = ShellIconMaker(self)
        self._iconMaker.updateIcon("busy")  # Busy initializing

        # Create timer
        self._elapsedTimesTimer = QtCore.QTimer(self)
        self._elapsedTimesTimer.setInterval(1000)
        self._elapsedTimesTimer.setSingleShot(False)
        self._elapsedTimesTimer.timeout.connect(self.onElapsedTimesTimer)
Beispiel #18
0
class Installer(QtWidgets.QDialog):

    lineFromStdOut = QtCore.Signal(str)

    def __init__(self, parent=None):
        QtWidgets.QDialog.__init__(self, parent)
        self.setWindowTitle("Install miniconda")
        self.setModal(True)
        self.resize(500, 500)

        text = translate(
            "bootstrapconda",
            "This will download and install miniconda on your computer.",
        )

        self._label = QtWidgets.QLabel(text, self)

        self._scipystack = QtWidgets.QCheckBox(
            translate("bootstrapconda", "Also install scientific packages"), self
        )
        self._scipystack.setChecked(True)
        self._path = QtWidgets.QLineEdit(default_conda_dir, self)
        self._progress = QtWidgets.QProgressBar(self)
        self._outputLine = QtWidgets.QLabel(self)
        self._output = QtWidgets.QPlainTextEdit(self)
        self._output.setReadOnly(True)
        self._button = QtWidgets.QPushButton("Install", self)

        self._outputLine.setSizePolicy(
            QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Fixed
        )

        vbox = QtWidgets.QVBoxLayout(self)
        self.setLayout(vbox)
        vbox.addWidget(self._label, 0)
        vbox.addWidget(self._path, 0)
        vbox.addWidget(self._scipystack, 0)
        vbox.addWidget(self._progress, 0)
        vbox.addWidget(self._outputLine, 0)
        vbox.addWidget(self._output, 1)
        vbox.addWidget(self._button, 0)

        self._button.clicked.connect(self.go)

        self.addOutput(translate("bootstrapconda", "Waiting to start installation.\n"))
        self._progress.setVisible(False)

        self.lineFromStdOut.connect(self.setStatus)

    def setStatus(self, line):
        self._outputLine.setText(line)

    def addOutput(self, text):
        # self._output.setPlainText(self._output.toPlainText() + '\n' + text)
        cursor = self._output.textCursor()
        cursor.movePosition(cursor.End, cursor.MoveAnchor)
        cursor.insertText(text)
        cursor.movePosition(cursor.End, cursor.MoveAnchor)
        self._output.setTextCursor(cursor)
        self._output.ensureCursorVisible()

    def addStatus(self, line):
        self.addOutput("\n" + line)
        self.setStatus(line)

    def go(self):

        # Check if we can install
        try:
            self._conda_dir = self._path.text()
            if not os.path.isabs(self._conda_dir):
                raise ValueError("Given installation path must be absolute.")
            if os.path.exists(self._conda_dir):
                raise ValueError("The given installation path already exists.")
        except Exception as err:
            self.addOutput("\nCould not install:\n" + str(err))
            return

        ok = False

        try:

            # Disable user input, get ready for installation
            self._progress.setVisible(True)
            self._button.clicked.disconnect()
            self._button.setEnabled(False)
            self._scipystack.setEnabled(False)
            self._path.setEnabled(False)

            if not os.path.exists(self._conda_dir):
                self.addStatus("Downloading installer ... ")
                self._progress.setMaximum(100)
                self.download()
                self.addStatus("Done downloading installer.")
                self.make_done()

                self.addStatus("Installing (this can take a minute) ... ")
                self._progress.setMaximum(0)
                ret = self.install()
                self.addStatus(("Failed" if ret else "Done") + " installing.")
                self.make_done()

            self.post_install()

            if self._scipystack.isChecked():
                self.addStatus("Installing scientific packages ... ")
                self._progress.setMaximum(0)
                ret = self.install_scipy()
                self.addStatus("Done installing scientific packages")
                self.make_done()

            self.addStatus("Verifying ... ")
            self._progress.setMaximum(100)
            ret = self.verify()
            if ret:
                self.addOutput("Error\n" + ret)
                self.addStatus("Verification Failed!")
            else:
                self.addOutput("Done verifying")
                self.addStatus("Ready to go!")
                self.make_done()
                ok = True

        except Exception as err:
            self.addStatus("Installation failed ...")
            self.addOutput("\n\nException!\n" + str(err))

        if not ok:
            self.addOutput("\n\nWe recommend installing miniconda or anaconda, ")
            self.addOutput("and making Pyzo aware if it via the shell configuration.")
        else:
            self.addOutput(
                '\n\nYou can install additional packages by running "conda install" in the shell.'
            )

        # Wrap up, allow user to exit
        self._progress.hide()
        self._button.setEnabled(True)
        self._button.setText("Close")
        self._button.clicked.connect(self.close)

    def make_done(self):
        self._progress.setMaximum(100)
        self._progress.setValue(100)
        etime = time.time() + 0.2
        while time.time() < etime:
            time.sleep(0.01)
            QtWidgets.qApp.processEvents()

    def download(self):

        # Installer already downloaded?
        if os.path.isfile(miniconda_path):
            self.addOutput("Already downloaded.")
            return  # os.remove(miniconda_path)

        # Get url key
        key = ""
        if sys.platform.startswith("win"):
            key = "win"
        elif sys.platform.startswith("darwin"):
            key = "osx"
        elif sys.platform.startswith("linux"):
            key = "linux"
        key += "64" if is_64bit() else "32"

        # Get url
        if key not in links:
            raise RuntimeError("Cannot download miniconda for this platform.")
        url = base_url + links[key]

        _fetch_file(url, miniconda_path, self._progress)

    def install(self):
        dest = self._conda_dir

        # Clear dir
        assert not os.path.isdir(dest), "Miniconda dir already exists"
        assert " " not in dest, "miniconda dest path must not contain spaces"

        if sys.platform.startswith("win"):
            return self._run_process([miniconda_path, "/S", "/D=%s" % dest])
        else:
            os.chmod(miniconda_path, os.stat(miniconda_path).st_mode | stat.S_IEXEC)
            return self._run_process([miniconda_path, "-b", "-p", dest])

    def post_install(self):

        exe = py_exe(self._conda_dir)

        # Add Pyzo channel
        cmd = [exe, "-m", "conda", "config", "--system", "--add", "channels", "pyzo"]
        subprocess.check_call(cmd, shell=sys.platform.startswith("win"))
        self.addStatus("Added Pyzo channel to conda env")

        # Add to pyzo shell config
        if pyzo.config.shellConfigs2 and pyzo.config.shellConfigs2[0]["exe"] == exe:
            pass
        else:
            s = pyzo.ssdf.new()
            s.name = "Py3-conda"
            s.exe = exe
            s.gui = "PyQt4"
            pyzo.config.shellConfigs2.insert(0, s)
            pyzo.saveConfig()
            self.addStatus("Prepended new env to Pyzo shell configs.")

    def install_scipy(self):

        packages = [
            "numpy",
            "scipy",
            "pandas",
            "matplotlib",
            "sympy",
            #'scikit-image', 'scikit-learn',
            "pyopengl",  # 'visvis', 'imageio',
            "tornado",
            "pyqt",  #'ipython', 'jupyter',
            #'requests', 'pygments','pytest',
        ]
        exe = py_exe(self._conda_dir)
        cmd = [exe, "-m", "conda", "install", "--yes"] + packages
        return self._run_process(cmd)

    def _run_process(self, cmd):
        """Run command in a separate process, catch stdout, show lines
        in the output label. On fail, show all output in output text.
        """
        p = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            shell=sys.platform.startswith("win"),
        )
        catcher = StreamCatcher(p.stdout, self.lineFromStdOut)

        while p.poll() is None:
            time.sleep(0.01)
            QtWidgets.qApp.processEvents()

        catcher.join()
        if p.poll():
            self.addOutput(catcher.output())
        return p.poll()

    def verify(self):

        self._progress.setValue(1)
        if not os.path.isdir(self._conda_dir):
            return "Conda dir not created."

        self._progress.setValue(11)
        exe = py_exe(self._conda_dir)
        if not os.path.isfile(exe):
            return "Conda dir does not have Python exe"

        self._progress.setValue(21)
        try:
            ver = subprocess.check_output([exe, "-c", "import sys; print(sys.version)"])
        except Exception as err:
            return "Error getting Python version: " + str(err)

        self._progress.setValue(31)
        if ver.decode() < "3.4":
            return "Expected Python version 3.4 or higher"

        self._progress.setValue(41)
        try:
            ver = subprocess.check_output(
                [exe, "-c", "import conda; print(conda.__version__)"]
            )
        except Exception as err:
            return "Error calling Python exe: " + str(err)

        self._progress.setValue(51)
        if ver.decode() < "3.16":
            return "Expected Conda version 3.16 or higher"

        # Smooth toward 100%
        for i in range(self._progress.value(), 100, 5):
            time.sleep(0.05)
            self._progress.setValue(i)
            QtWidgets.qApp.processEvents()
Beispiel #19
0
class ShellStackWidget(QtWidgets.QWidget):
    """The shell stack widget provides a stack of shells.

    It wrapps a QStackedWidget that contains the shell objects. This
    stack is used as a reference to synchronize the shell selection with.
    We keep track of what is the current selected shell and apply updates
    if necessary. Therefore, changing the current shell in the stack
    should be enough to invoke a full update.

    """

    # When the current shell changes.
    currentShellChanged = QtCore.Signal()

    # When the current shells state (or debug state) changes,
    # or when a new prompt is received.
    # Also fired when the current shell changes.
    currentShellStateChanged = QtCore.Signal()

    def __init__(self, parent):
        QtWidgets.QWidget.__init__(self, parent)

        # create toolbar
        self._toolbar = QtWidgets.QToolBar(self)
        self._toolbar.setMaximumHeight(26)
        self._toolbar.setIconSize(QtCore.QSize(16, 16))

        # create stack
        self._stack = QtWidgets.QStackedWidget(self)

        # Populate toolbar
        self._shellButton = ShellControl(self._toolbar, self._stack)
        self._debugmode = 0
        self._dbs = DebugStack(self._toolbar)
        #
        self._toolbar.addWidget(self._shellButton)
        self._toolbar.addSeparator()
        # self._toolbar.addWidget(self._dbc) -> delayed, see addContextMenu()

        self._interpreterhelp = InterpreterHelper(self)

        # widget layout
        layout = QtWidgets.QVBoxLayout()
        layout.setSpacing(0)
        # set margins
        margin = pyzo.config.view.widgetMargin
        layout.setContentsMargins(margin, margin, margin, margin)

        layout.addWidget(self._toolbar)
        layout.addWidget(self._stack, 0)
        layout.addWidget(self._interpreterhelp, 0)
        self.setLayout(layout)

        # make callbacks
        self._stack.currentChanged.connect(self.onCurrentChanged)

        self.showInterpreterHelper()

    def __iter__(self):
        i = 0
        while i < self._stack.count():
            w = self._stack.widget(i)
            i += 1
            yield w

    def showInterpreterHelper(self, show=True):
        self._interpreterhelp.setVisible(show)
        self._toolbar.setVisible(not show)
        self._stack.setVisible(not show)
        if show:
            self._interpreterhelp.detect()

    def addShell(self, shellInfo=None):
        """addShell()
        Add a shell to the widget."""

        # Create shell and add to stack
        shell = PythonShell(self, shellInfo)
        self._stack.addWidget(shell)
        # Bind to signals
        shell.stateChanged.connect(self.onShellStateChange)
        shell.debugStateChanged.connect(self.onShellDebugStateChange)
        # Select it and focus on it (invokes onCurrentChanged)
        self._stack.setCurrentWidget(shell)
        shell.setFocus()
        return shell

    def removeShell(self, shell):
        """removeShell()
        Remove an existing shell from the widget
        """
        self._stack.removeWidget(shell)

    def onCurrentChanged(self, index):
        """When another shell is selected, update some things."""

        # Get current
        shell = self.getCurrentShell()
        # Call functions
        self.onShellStateChange(shell)
        self.onShellDebugStateChange(shell)
        # Emit Signal
        self.currentShellChanged.emit()

    def onShellStateChange(self, shell):
        """Called when the shell state changes, and is called
        by onCurrentChanged. Sets the mainwindow's icon if busy.
        """

        # Keep shell button and its menu up-to-date
        self._shellButton.updateShellMenu(shell)

        if shell is self.getCurrentShell():  # can be None
            # Update application icon
            if shell and shell._state in ["Busy"]:
                pyzo.main.setWindowIcon(pyzo.iconRunning)
            else:
                pyzo.main.setWindowIcon(pyzo.icon)
            # Send signal
            self.currentShellStateChanged.emit()

    def onShellDebugStateChange(self, shell):
        """Called when the shell debug state changes, and is called
        by onCurrentChanged. Sets the debug button.
        """

        if shell is self.getCurrentShell():

            # Update debug info
            if shell and shell._debugState:
                info = shell._debugState
                self._debugmode = info["debugmode"]
                for action in self._debugActions:
                    action.setEnabled(self._debugmode == 2)
                self._debugActions[-1].setEnabled(self._debugmode > 0)  # Stop
                self._dbs.setTrace(shell._debugState)
            else:
                for action in self._debugActions:
                    action.setEnabled(False)
                self._debugmode = 0
                self._dbs.setTrace(None)
            # Send signal
            self.currentShellStateChanged.emit()

    def getCurrentShell(self):
        """getCurrentShell()
        Get the currently active shell.
        """

        w = None
        if self._stack.count():
            w = self._stack.currentWidget()
        if not w:
            return None
        else:
            return w

    def getShells(self):
        """Get all shell in stack as list"""

        shells = []
        for i in range(self._stack.count()):
            shell = self.getShellAt(i)
            if shell is not None:
                shells.append(shell)

        return shells

    def getShellAt(self, i):
        return
        """ Get shell at current tab index """

        return self._stack.widget(i)

    def addContextMenu(self):
        # A bit awkward... but the ShellMenu needs the ShellStack, so it
        # can only be initialized *after* the shellstack is created ...

        # Give shell tool button a menu
        self._shellButton.setMenu(ShellButtonMenu(self, "Shell button menu"))
        self._shellButton.menu().aboutToShow.connect(
            self._shellButton._elapsedTimesTimer.start)

        # Also give it a context menu
        self._shellButton.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self._shellButton.customContextMenuRequested.connect(
            self.contextMenuTriggered)

        # Add actions
        for action in pyzo.main.menuBar()._menumap["shell"]._shellActions:
            action = self._toolbar.addAction(action)

        self._toolbar.addSeparator()

        # Add debug actions
        self._debugActions = []
        for action in pyzo.main.menuBar()._menumap["shell"]._shellDebugActions:
            self._debugActions.append(action)
            action = self._toolbar.addAction(action)

        # Delayed-add debug control buttons
        self._toolbar.addWidget(self._dbs)

    def contextMenuTriggered(self, p):
        """Called when context menu is clicked"""

        # Get index of shell belonging to the tab
        shell = self.getCurrentShell()

        if shell:
            p = self._shellButton.mapToGlobal(
                self._shellButton.rect().bottomLeft())
            ShellTabContextMenu(shell=shell, parent=self).popup(p)

    def onShellAction(self, action):
        shell = self.getCurrentShell()
        if shell:
            getattr(shell, action)()
Beispiel #20
0
    def __init__(self, parent):
        QtWidgets.QWidget.__init__(self, parent)

        # Make sure there is a configuration entry for this tool
        # The pyzo tool manager makes sure that there is an entry in
        # config.tools before the tool is instantiated.
        toolId = self.__class__.__name__.lower()
        self._config = pyzo.config.tools[toolId]
        if not hasattr(self._config, "showTypes"):
            self._config.showTypes = ["class", "def", "cell", "todo"]
        if not hasattr(self._config, "level"):
            self._config.level = 2

        # Keep track of clicks so we can "go back"
        self._nav = {}  # editor-id -> Navigation object

        # Create buttons for navigation
        self._navbut_back = QtWidgets.QToolButton(self)
        self._navbut_back.setIcon(pyzo.icons.arrow_left)
        self._navbut_back.setIconSize(QtCore.QSize(16, 16))
        self._navbut_back.setStyleSheet(
            "QToolButton { border: none; padding: 0px; }")
        self._navbut_back.clicked.connect(self.onNavBack)
        #
        self._navbut_forward = QtWidgets.QToolButton(self)
        self._navbut_forward.setIcon(pyzo.icons.arrow_right)
        self._navbut_forward.setIconSize(QtCore.QSize(16, 16))
        self._navbut_forward.setStyleSheet(
            "QToolButton { border: none; padding: 0px; }")
        self._navbut_forward.clicked.connect(self.onNavForward)

        # # Create icon for slider
        # self._sliderIcon = QtWidgets.QToolButton(self)
        # self._sliderIcon.setIcon(pyzo.icons.text_align_right)
        # self._sliderIcon.setIconSize(QtCore.QSize(16,16))
        # self._sliderIcon.setStyleSheet("QToolButton { border: none; padding: 0px; }")

        # Create slider
        self._slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
        self._slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
        self._slider.setSingleStep(1)
        self._slider.setPageStep(1)
        self._slider.setRange(1, 5)
        self._slider.setValue(self._config.level)
        self._slider.valueChanged.connect(self.updateStructure)

        # Create options button
        # self._options = QtWidgets.QPushButton(self)
        # self._options.setText('Options'))
        # self._options.setToolTip("What elements to show.")
        self._options = QtWidgets.QToolButton(self)
        self._options.setIcon(pyzo.icons.filter)
        self._options.setIconSize(QtCore.QSize(16, 16))
        self._options.setPopupMode(self._options.InstantPopup)
        self._options.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)

        # Create options menu
        self._options._menu = QtWidgets.QMenu()
        self._options.setMenu(self._options._menu)

        # Create tree widget
        self._tree = QtWidgets.QTreeWidget(self)
        self._tree.setHeaderHidden(True)
        self._tree.itemCollapsed.connect(self.updateStructure)  # keep expanded
        self._tree.itemClicked.connect(self.onItemClick)

        # Create two sizers
        self._sizer1 = QtWidgets.QVBoxLayout(self)
        self._sizer2 = QtWidgets.QHBoxLayout()
        self._sizer1.setSpacing(2)
        # set margins
        margin = pyzo.config.view.widgetMargin
        self._sizer1.setContentsMargins(margin, margin, margin, margin)

        # Set layout
        self._sizer1.addLayout(self._sizer2, 0)
        self._sizer1.addWidget(self._tree, 1)
        # self._sizer2.addWidget(self._sliderIcon, 0)
        self._sizer2.addWidget(self._navbut_back, 0)
        self._sizer2.addWidget(self._navbut_forward, 0)
        self._sizer2.addStretch(1)
        self._sizer2.addWidget(self._slider, 6)
        self._sizer2.addStretch(1)
        self._sizer2.addWidget(self._options, 0)
        #
        self.setLayout(self._sizer1)

        # Init current-file name
        self._currentEditorId = 0

        # Bind to events
        pyzo.editors.currentChanged.connect(self.onEditorsCurrentChanged)
        pyzo.editors.parserDone.connect(self.updateStructure)

        self._options.pressed.connect(self.onOptionsPress)
        self._options._menu.triggered.connect(self.onOptionMenuTiggered)

        # Start
        # When the tool is loaded, the editorStack is already done loading
        # all previous files and selected the appropriate file.
        self.onOptionsPress()  # Create menu now
        self.onEditorsCurrentChanged()
Beispiel #21
0
    def __init__(self, parent):
        QtWidgets.QWidget.__init__(self, parent)

        # Create text field, checkbox, and button
        self._text = QtWidgets.QLineEdit(self)
        self._printBut = QtWidgets.QPushButton("Print", self)

        style = QtWidgets.qApp.style()

        self._backBut = QtWidgets.QToolButton(self)
        self._backBut.setIcon(style.standardIcon(style.SP_ArrowLeft))
        self._backBut.setIconSize(QtCore.QSize(16, 16))
        self._backBut.setPopupMode(self._backBut.DelayedPopup)
        self._backBut.setMenu(
            PyzoInteractiveHelpHistoryMenu("Backward menu", self, False))

        self._forwBut = QtWidgets.QToolButton(self)
        self._forwBut.setIcon(style.standardIcon(style.SP_ArrowRight))
        self._forwBut.setIconSize(QtCore.QSize(16, 16))
        self._forwBut.setPopupMode(self._forwBut.DelayedPopup)
        self._forwBut.setMenu(
            PyzoInteractiveHelpHistoryMenu("Forward menu", self, True))

        # Create options button
        self._options = QtWidgets.QToolButton(self)
        self._options.setIcon(pyzo.icons.wrench)
        self._options.setIconSize(QtCore.QSize(16, 16))
        self._options.setPopupMode(self._options.InstantPopup)
        self._options.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)

        # Create options menu
        self._options._menu = QtWidgets.QMenu()
        self._options.setMenu(self._options._menu)

        # Create browser
        self._browser = QtWidgets.QTextBrowser(self)
        self._browser_text = initText
        self._browser.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self._browser.customContextMenuRequested.connect(self.showMenu)

        # Create two sizers
        self._sizer1 = QtWidgets.QVBoxLayout(self)
        self._sizer2 = QtWidgets.QHBoxLayout()

        # Put the elements together
        self._sizer2.addWidget(self._backBut, 1)
        self._sizer2.addWidget(self._forwBut, 2)
        self._sizer2.addWidget(self._text, 4)
        self._sizer2.addWidget(self._printBut, 0)
        self._sizer2.addWidget(self._options, 3)
        #
        self._sizer1.addLayout(self._sizer2, 0)
        self._sizer1.addWidget(self._browser, 1)
        #
        self._sizer1.setSpacing(2)
        # set margins
        margin = pyzo.config.view.widgetMargin
        self._sizer1.setContentsMargins(margin, margin, margin, margin)

        self.setLayout(self._sizer1)

        # Set config
        toolId = self.__class__.__name__.lower()
        self._config = config = pyzo.config.tools[toolId]
        #
        if not hasattr(config, "smartNewlines"):
            config.smartNewlines = True
        if not hasattr(config, "fontSize"):
            if sys.platform == "darwin":
                config.fontSize = 12
            else:
                config.fontSize = 10

        # Create callbacks
        self._text.returnPressed.connect(self.queryDoc)
        self._printBut.clicked.connect(self.printDoc)
        self._backBut.clicked.connect(self.goBack)
        self._forwBut.clicked.connect(self.goForward)
        #
        self._options.pressed.connect(self.onOptionsPress)
        self._options._menu.triggered.connect(self.onOptionMenuTiggered)

        # Start
        self._history = []
        self._histindex = 0
        self.setText()  # Set default text
        self.onOptionsPress()  # Fill menu
Beispiel #22
0
class WorkspaceProxy(QtCore.QObject):
    """WorkspaceProxy

    A proxy class to handle the asynchonous behaviour of getting information
    from the shell. The workspace tool asks for a certain name, and this
    class notifies when new data is available using a qt signal.

    """

    haveNewData = QtCore.Signal()

    def __init__(self):
        QtCore.QObject.__init__(self)

        # Variables
        self._variables = []

        # Element to get more info of
        self._name = ""

        # Bind to events
        pyzo.shells.currentShellChanged.connect(self.onCurrentShellChanged)
        pyzo.shells.currentShellStateChanged.connect(
            self.onCurrentShellStateChanged)

        # Initialize
        self.onCurrentShellStateChanged()

    def addNamePart(self, part):
        """addNamePart(part)
        Add a part to the name.
        """
        parts = splitName(self._name)
        parts.append(part)
        self.setName(joinName(parts))

    def setName(self, name):
        """setName(name)
        Set the name that we want to know more of.
        """
        self._name = name

        shell = pyzo.shells.getCurrentShell()
        if shell:
            future = shell._request.dir2(self._name)
            future.add_done_callback(self.processResponse)

    def goUp(self):
        """goUp()
        Cut the last part off the name.
        """
        parts = splitName(self._name)
        if parts:
            parts.pop()
        self.setName(joinName(parts))

    def onCurrentShellChanged(self):
        """onCurrentShellChanged()
        When no shell is selected now, update this. In all other cases,
        the onCurrentShellStateChange will be fired too.
        """
        shell = pyzo.shells.getCurrentShell()
        if not shell:
            self._variables = []
            self.haveNewData.emit()

    def onCurrentShellStateChanged(self):
        """onCurrentShellStateChanged()
        Do a request for information!
        """
        shell = pyzo.shells.getCurrentShell()
        if not shell:
            # Should never happen I think, but just to be sure
            self._variables = []
        elif shell._state.lower() != "busy":
            future = shell._request.dir2(self._name)
            future.add_done_callback(self.processResponse)

    def processResponse(self, future):
        """processResponse(response)
        We got a response, update our list and notify the tree.
        """

        response = []

        # Process future
        if future.cancelled():
            pass  # print('Introspect cancelled') # No living kernel
        elif future.exception():
            print("Introspect-queryDoc-exception: ", future.exception())
        else:
            response = future.result()

        self._variables = response
        self.haveNewData.emit()
Beispiel #23
0
    def _alignRecursive(self):
        """_alignRecursive()

        Recursive alignment of the items. The alignment process
        should be initiated from alignTabs().

        """

        # Only if visible
        if not self.isVisible():
            return

        # Get tab bar and number of items
        N = self.count()

        # Get right edge of last tab and left edge of corner widget
        pos1 = self.tabRect(0).topLeft()
        pos2 = self.tabRect(N - 1).topRight()
        cornerWidget = self.parent().cornerWidget()
        if cornerWidget:
            pos3 = cornerWidget.pos()
        else:
            pos3 = QtCore.QPoint(int(self.width()), 0)
        x1 = pos1.x()
        x2 = pos2.x()
        x3 = pos3.x()
        alignMargin = x3 - (x2 - x1) - 3  # Must be positive (has margin)

        # Are the tabs too wide?
        if alignMargin < 0:
            # Tabs extend beyond corner widget

            # Reduce width then
            self._alignWidth -= 1
            self._alignWidth = max(self._alignWidth, MIN_NAME_WIDTH)

            # Apply
            self._setMaxWidthOfAllItems()
            self._alignWidthIsReducing = True

            # Try again if there's still room for reduction
            if self._alignWidth > MIN_NAME_WIDTH:
                self._alignTimer.start()

        elif alignMargin > 10 and not self._alignWidthIsReducing:
            # Gap between tabs and corner widget is a bit large

            # Increase width then
            self._alignWidth += 1
            self._alignWidth = min(self._alignWidth, MAX_NAME_WIDTH)

            # Apply
            itemsElided = self._setMaxWidthOfAllItems()

            # Try again if there's still room for increment
            if itemsElided and self._alignWidth < MAX_NAME_WIDTH:
                self._alignTimer.start()
                # self._alignTimer.timeout.emit()

        else:
            pass  # margin is good
Beispiel #24
0
class StyleEdit(QtWidgets.QWidget):
    """The StyleLineEdit is a line that allows the edition
    of one style (i.e. "Editor.Text" or  "Syntax.identifier")
    with a given StyleElementDescription it find the editable
    parts and display the adaptated widgets for edition
    (checkbok for bold and italic, combo box for linestyles...).
    """

    styleChanged = QtCore.Signal(str, str)

    def __init__(self, defaultStyle, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # The styleKey is sent with the styleChanged signal for easy identification
        self.styleKey = defaultStyle.key

        self.layout = layout = QtWidgets.QHBoxLayout()
        # The setters are used when setting the style
        self.setters = {}

        # TODO: the use of StyleFormat._parts should be avoided
        # We use the StyleFormat._parts keys, to find the elements
        # Useful to edits, because the property may return a value
        # Even if they were not defined in the defaultFormat
        fmtParts = defaultStyle.defaultFormat._parts

        # Add the widgets corresponding to the fields
        if "fore" in fmtParts:
            self.__add_clrLineEdit("fore", "Foreground")
        if "back" in fmtParts:
            self.__add_clrLineEdit("back", "Background")
        if "bold" in fmtParts:
            self.__add_checkBox("bold", "Bold")
        if "italic" in fmtParts:
            self.__add_checkBox("italic", "Italic")
        if "underline" in fmtParts:
            self.__add_comboBox("underline", "Underline", "No", "Dotted",
                                "Wave", "Full", "Yes")
        if "linestyle" in fmtParts:
            self.__add_comboBox("linestyle", "Linestyle", "Dashed", "Dotted",
                                "Full")

        self.setLayout(layout)

        self.setSizePolicy(QtWidgets.QSizePolicy.Minimum,
                           QtWidgets.QSizePolicy.Minimum)

    def __add_clrLineEdit(self, key, name):
        """this is a helper method to create a ColorLineEdit
        it adds the created widget (as a TitledWidget) to the layout and
        register a setter and listen to changes
        """
        clrEdit = ColorLineEdit(name)
        clrEdit.textChanged.connect(
            lambda txt, key=key: self.__update(key, txt))
        self.setters[key] = clrEdit.setText
        self.layout.addWidget(TitledWidget(name, clrEdit), 0)

    def __add_checkBox(self, key, name):
        """this is a helper method to create a QCheckBox
        it adds the created widget (as a TitledWidget) to the layout and
        register a setter and listen to changes
        """

        checkBox = QtWidgets.QCheckBox()

        self.setters[key] = lambda val, check=checkBox: check.setCheckState(
            val == "yes")

        checkBox.stateChanged.connect(lambda state, key=key: self.__update(
            key, "yes" if state else "no"))

        self.layout.addWidget(TitledWidget(name, checkBox))

    def __add_comboBox(self, key, name, *items):
        """this is a helper method to create a comboBox
        it adds the created widget (as a TitledWidget) to the layout and
        register a setter and listen to changes
        """

        combo = QtWidgets.QComboBox()
        combo.addItems(items)
        combo.currentTextChanged.connect(
            lambda txt, key=key: self.__update(key, txt))

        # Note: those setters may become problematic if
        # someone use the synonyms (defined in codeeditor/style.py)
        # i.e. a stylement is of form "linestyle:dashline"
        # instead of the "linestyle:dashed"
        self.setters[key] = lambda txt, cmb=combo: cmb.setCurrentText(
            txt.capitalize())
        self.layout.addWidget(TitledWidget(name, combo))

    def __update(self, key, value):
        """this function is called everytime one of the children
        widget data has been modified by the user"""
        self.styleChanged.emit(self.styleKey, key + ":" + value)

    def setStyle(self, text):
        """updates every children to match the StyleFormat(text) fields"""
        style = StyleFormat(text)
        for key, setter in self.setters.items():
            setter(style[key])

    def setFocus(self, val):
        self.layout.itemAt(0).widget().setFocus(True)
Beispiel #25
0
class CompactTabBar(QtWidgets.QTabBar):
    """CompactTabBar(parent, *args, padding=(4,4,6,6), preventEqualTexts=True)

    Tab bar corresponcing to the CompactTabWidget.

    With the "padding" argument the padding of the tabs can be chosen.
    It should be an integer, or a 4 element tuple specifying the padding
    for top, bottom, left, right. When a tab has a button,
    the padding is the space between button and text.

    With preventEqualTexts to True, will reduce the amount of eliding if
    two tabs have (partly) the same name, so that they can always be
    distinguished.

    """

    # Add signal to be notified of double clicks on tabs
    tabDoubleClicked = QtCore.Signal(int)
    barDoubleClicked = QtCore.Signal()

    def __init__(self, *args, padding=(4, 4, 6, 6), preventEqualTexts=True):
        QtWidgets.QTabBar.__init__(self, *args)

        # Put tab widget in document mode
        self.setDocumentMode(True)

        # Widget needs to draw its background (otherwise Mac has a dark bg)
        self.setDrawBase(False)
        if sys.platform == "darwin":
            self.setAutoFillBackground(True)

        # Set whether we want to prevent eliding for names that start the same.
        self._preventEqualTexts = preventEqualTexts

        # Allow moving tabs around
        self.setMovable(True)

        # Get padding
        if isinstance(padding, (int, float)):
            padding = padding, padding, padding, padding
        elif isinstance(padding, (tuple, list)):
            pass
        else:
            raise ValueError("Invalid value for padding.")

        # Set style sheet
        stylesheet = STYLESHEET
        stylesheet = stylesheet.replace("PADDING_TOP", str(padding[0]))
        stylesheet = stylesheet.replace("PADDING_BOTTOM", str(padding[1]))
        stylesheet = stylesheet.replace("PADDING_LEFT", str(padding[2]))
        stylesheet = stylesheet.replace("PADDING_RIGHT", str(padding[3]))
        self.setStyleSheet(stylesheet)

        # We do our own eliding
        self.setElideMode(QtCore.Qt.ElideNone)

        # Make tabs wider if there's plenty space?
        self.setExpanding(False)

        # If there's not enough space, use scroll buttons
        self.setUsesScrollButtons(True)

        # When a tab is removed, select previous
        self.setSelectionBehaviorOnRemove(self.SelectPreviousTab)

        # Init alignment parameters
        self._alignWidth = MIN_NAME_WIDTH  # Width in characters
        self._alignWidthIsReducing = False  # Whether in process of reducing

        # Create timer for aligning
        self._alignTimer = QtCore.QTimer(self)
        self._alignTimer.setInterval(10)
        self._alignTimer.setSingleShot(True)
        self._alignTimer.timeout.connect(self._alignRecursive)

    def _compactTabBarData(self, i):
        """_compactTabBarData(i)

        Get the underlying tab data for tab i. Only for internal use.

        """

        # Get current TabData instance
        tabData = QtWidgets.QTabBar.tabData(self, i)
        if (tabData is not None) and hasattr(tabData, "toPyObject"):
            tabData = tabData.toPyObject()  # Older version of Qt

        # If none, make it as good as we can
        if not tabData:
            name = str(QtWidgets.QTabBar.tabText(self, i))
            tabData = TabData(name)
            QtWidgets.QTabBar.setTabData(self, i, tabData)

        # Done
        return tabData

    ## Overload a few methods

    def mouseDoubleClickEvent(self, event):
        i = self.tabAt(event.pos())
        if i == -1:
            # There was no tab under the cursor
            self.barDoubleClicked.emit()
        else:
            # Tab was double clicked
            self.tabDoubleClicked.emit(i)

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.MiddleButton:
            i = self.tabAt(event.pos())
            if i >= 0:
                self.parent().tabCloseRequested.emit(i)
                return
        super().mousePressEvent(event)

    def setTabData(self, i, data):
        """setTabData(i, data)

        Set the given object at the tab with index 1.

        """
        # Get underlying python instance
        tabData = self._compactTabBarData(i)

        # Attach given data
        tabData.data = data

    def tabData(self, i):
        """tabData(i)

        Get the tab data at item i. Always returns a Python object.

        """

        # Get underlying python instance
        tabData = self._compactTabBarData(i)

        # Return stored data
        return tabData.data

    def setTabText(self, i, text):
        """setTabText(i, text)

        Set the text for tab i.

        """
        tabData = self._compactTabBarData(i)
        if text != tabData.name:
            tabData.name = text
            self.alignTabs()

    def tabText(self, i):
        """tabText(i)

        Get the title of the tab at index i.

        """
        tabData = self._compactTabBarData(i)
        return tabData.name

    ## Overload events and protected functions

    def tabInserted(self, i):
        QtWidgets.QTabBar.tabInserted(self, i)

        # Is called when a tab is inserted

        # Get given name and store
        name = str(QtWidgets.QTabBar.tabText(self, i))
        tabData = TabData(name)
        QtWidgets.QTabBar.setTabData(self, i, tabData)

        # Update
        self.alignTabs()

    def tabRemoved(self, i):
        QtWidgets.QTabBar.tabRemoved(self, i)

        # Update
        self.alignTabs()

    def resizeEvent(self, event):
        QtWidgets.QTabBar.resizeEvent(self, event)
        self.alignTabs()

    def showEvent(self, event):
        QtWidgets.QTabBar.showEvent(self, event)
        self.alignTabs()

    ## For aligning

    def alignTabs(self):
        """alignTabs()

        Align the tab items. Their names are ellided if required so that
        all tabs fit on the tab bar if possible. When there is too little
        space, the QTabBar will kick in and draw scroll arrows.

        """

        # Set name widths correct (in case new names were added)
        self._setMaxWidthOfAllItems()

        # Start alignment process
        self._alignWidthIsReducing = False
        self._alignTimer.start()

    def _alignRecursive(self):
        """_alignRecursive()

        Recursive alignment of the items. The alignment process
        should be initiated from alignTabs().

        """

        # Only if visible
        if not self.isVisible():
            return

        # Get tab bar and number of items
        N = self.count()

        # Get right edge of last tab and left edge of corner widget
        pos1 = self.tabRect(0).topLeft()
        pos2 = self.tabRect(N - 1).topRight()
        cornerWidget = self.parent().cornerWidget()
        if cornerWidget:
            pos3 = cornerWidget.pos()
        else:
            pos3 = QtCore.QPoint(int(self.width()), 0)
        x1 = pos1.x()
        x2 = pos2.x()
        x3 = pos3.x()
        alignMargin = x3 - (x2 - x1) - 3  # Must be positive (has margin)

        # Are the tabs too wide?
        if alignMargin < 0:
            # Tabs extend beyond corner widget

            # Reduce width then
            self._alignWidth -= 1
            self._alignWidth = max(self._alignWidth, MIN_NAME_WIDTH)

            # Apply
            self._setMaxWidthOfAllItems()
            self._alignWidthIsReducing = True

            # Try again if there's still room for reduction
            if self._alignWidth > MIN_NAME_WIDTH:
                self._alignTimer.start()

        elif alignMargin > 10 and not self._alignWidthIsReducing:
            # Gap between tabs and corner widget is a bit large

            # Increase width then
            self._alignWidth += 1
            self._alignWidth = min(self._alignWidth, MAX_NAME_WIDTH)

            # Apply
            itemsElided = self._setMaxWidthOfAllItems()

            # Try again if there's still room for increment
            if itemsElided and self._alignWidth < MAX_NAME_WIDTH:
                self._alignTimer.start()
                # self._alignTimer.timeout.emit()

        else:
            pass  # margin is good

    def _getAllNames(self):
        """_getAllNames()

        Get a list of all (full) tab names.

        """
        return [self._compactTabBarData(i).name for i in range(self.count())]

    def _setMaxWidthOfAllItems(self):
        """_setMaxWidthOfAllItems()

        Sets the maximum width of all items now, by eliding the names.
        Returns whether any items were elided.

        """

        # Get whether an item was reduced in size
        itemReduced = False

        for i in range(self.count()):

            # Get width
            w = self._alignWidth

            # Get name
            name = self._compactTabBarData(i).name

            # If its too long, first make it shorter by stripping dir names
            if (w + 1) < len(name) and "/" in name:
                name = name.split("/")[-1]

            # Check if we can reduce the name size, correct w if necessary
            if ((w + 1) < len(name)) and self._preventEqualTexts:

                # Increase w untill there are no names that start the same
                allNames = self._getAllNames()
                hasSimilarNames = True
                diff = 2
                w -= 1
                while hasSimilarNames and w < len(name):
                    w += 1
                    w2 = w - (diff - 1)
                    shortName = name[:w2]
                    similarnames = [n for n in allNames if n[:w2] == shortName]
                    hasSimilarNames = len(similarnames) > 1

            # Check again, with corrected w
            if (w + 1) < len(name):
                name = name[:w] + ELLIPSIS
                itemReduced = True

            # Set text now
            QtWidgets.QTabBar.setTabText(self, i, name)

        # Done
        return itemReduced
Beispiel #26
0
class WebView(QtWidgets.QTextBrowser):
    """Inherit the webview class to implement zooming using
    the mouse wheel.
    """

    loadStarted = QtCore.Signal()
    loadFinished = QtCore.Signal(bool)

    def __init__(self, parent):
        QtWidgets.QTextBrowser.__init__(self, parent)

        # Current url
        self._url = ""
        self._history = []
        self._history2 = []

        # Connect
        self.anchorClicked.connect(self.load)

    def wheelEvent(self, event):
        # Zooming does not work for this widget
        if QtCore.Qt.ControlModifier & QtWidgets.qApp.keyboardModifiers():
            self.parent().wheelEvent(event)
        else:
            QtWidgets.QTextBrowser.wheelEvent(self, event)

    def url(self):
        return self._url

    def _getUrlParts(self):
        r = urllib.parse.urlparse(self._url)
        base = r.scheme + "://" + r.netloc
        return base, r.path, r.fragment

    #
    #     def loadCss(self, urls=[]):
    #         urls.append('http://docs.python.org/_static/default.css')
    #         urls.append('http://docs.python.org/_static/pygments.css')
    #         text = ''
    #         for url in urls:
    #             tmp = urllib.request.urlopen(url).read().decode('utf-8')
    #             text += '\n' + tmp
    #         self.document().setDefaultStyleSheet(text)

    def back(self):

        # Get url and update forward history
        url = self._history.pop()
        self._history2.append(self._url)

        # Go there
        url = self._load(url)

    def forward(self):

        if not self._history2:
            return

        # Get url and update forward history
        url = self._history2.pop()
        self._history.append(self._url)

        # Go there
        url = self._load(url)

    def load(self, url):

        # Clear forward history
        self._history2 = []

        # Store current url in history
        while self._url in self._history:
            self._history.remove(self._url)
        self._history.append(self._url)

        # Load
        url = self._load(url)

    def _load(self, url):
        """_load(url)
        Convert url and load page, returns new url.
        """
        # Make url a string
        if isinstance(url, QtCore.QUrl):
            url = str(url.toString())

        # Compose relative url to absolute
        if url.startswith("#"):
            base, path, frag = self._getUrlParts()
            url = base + path + url
        elif "//" not in url:
            base, path, frag = self._getUrlParts()
            url = base + "/" + url.lstrip("/")

        # Try loading
        self.loadStarted.emit()
        self._url = url
        try:
            # print('URL:', url)
            text = urllib.request.urlopen(url).read().decode("utf-8")
            self.setHtml(text)
            self.loadFinished.emit(True)
        except Exception as err:
            self.setHtml(str(err))
            self.loadFinished.emit(False)

        # Set
        return url
Beispiel #27
0
    def __init__(self, parent):
        QtWidgets.QFrame.__init__(self, parent)

        # Init config
        toolId = self.__class__.__name__.lower()
        self._config = pyzo.config.tools[toolId]
        if not hasattr(self._config, "zoomFactor"):
            self._config.zoomFactor = 1.0
        if not hasattr(self._config, "bookMarks"):
            self._config.bookMarks = []
        for item in default_bookmarks:
            if item not in self._config.bookMarks:
                self._config.bookMarks.append(item)

        # Get style object (for icons)
        style = QtWidgets.QApplication.style()

        # Create some buttons
        self._back = QtWidgets.QToolButton(self)
        self._back.setIcon(style.standardIcon(style.SP_ArrowBack))
        self._back.setIconSize(QtCore.QSize(16, 16))
        #
        self._forward = QtWidgets.QToolButton(self)
        self._forward.setIcon(style.standardIcon(style.SP_ArrowForward))
        self._forward.setIconSize(QtCore.QSize(16, 16))

        # Create address bar
        # self._address = QtWidgets.QLineEdit(self)
        self._address = QtWidgets.QComboBox(self)
        self._address.setEditable(True)
        self._address.setInsertPolicy(self._address.NoInsert)
        #
        for a in self._config.bookMarks:
            self._address.addItem(a)
        self._address.setEditText("")

        # Create web view
        if imported_qtwebkit:
            self._view = QtWebKit.QWebView(self)
        else:
            self._view = WebView(self)
        #
        #         self._view.setZoomFactor(self._config.zoomFactor)
        #         settings = self._view.settings()
        #         settings.setAttribute(settings.JavascriptEnabled, True)
        #         settings.setAttribute(settings.PluginsEnabled, True)

        # Layout
        self._sizer1 = QtWidgets.QVBoxLayout(self)
        self._sizer2 = QtWidgets.QHBoxLayout()
        #
        self._sizer2.addWidget(self._back, 0)
        self._sizer2.addWidget(self._forward, 0)
        self._sizer2.addWidget(self._address, 1)
        #
        self._sizer1.addLayout(self._sizer2, 0)
        self._sizer1.addWidget(self._view, 1)
        #
        self._sizer1.setSpacing(2)
        # set margins
        margin = pyzo.config.view.widgetMargin
        self._sizer1.setContentsMargins(margin, margin, margin, margin)

        self.setLayout(self._sizer1)

        # Bind signals
        self._back.clicked.connect(self.onBack)
        self._forward.clicked.connect(self.onForward)
        self._address.lineEdit().returnPressed.connect(self.go)
        self._address.activated.connect(self.go)
        self._view.loadFinished.connect(self.onLoadEnd)
        self._view.loadStarted.connect(self.onLoadStart)

        # Start
        self._view.show()
        self.go("http://docs.python.org")
Beispiel #28
0
class ThemeEditorWidget(QtWidgets.QWidget):
    """The ThemeEditorWidgets allows to edits themes,
    it has one StyleEdit widget per StyleElements ("Editor.Text",
    "Syntax.string"). It emits a signal on each style changes

    It also manages basic theme I/O :
        - adding new theme
        - renaming theme

    """

    styleChanged = QtCore.Signal(dict)
    done = QtCore.Signal(int)

    def __init__(self, themes, *args, editor=None, **kwargs):
        super().__init__(*args, **kwargs)

        # dict of themes, a deep copy of pyzo.themes
        self.themes = themes
        # We store the key name separate so we can easier track renames
        self.cur_theme_key = ""
        # The current theme being changed
        self.cur_theme = None

        # If an editor is given, connect to it
        self.editor = editor
        if self.editor is not None:
            self.editor.tokenClicked.connect(self.focusOnStyle)
            self.styleChanged.connect(self.editor.setStyle)

        # Display editables style formats in a scroll area
        self.scrollArea = scrollArea = QtWidgets.QScrollArea()
        self.scrollArea.setWidgetResizable(True)

        formLayout = QtWidgets.QFormLayout()
        self.styleEdits = {}

        # Add one pair of label and StyleEdit per style element description
        # to the formLayout and connect the StyleEdit signals to the updatedStyle method
        for styleDesc in pyzo.codeeditor.CodeEditor.getStyleElementDescriptions(
        ):
            label = QtWidgets.QLabel(text=styleDesc.name,
                                     toolTip=styleDesc.description)
            label.setWordWrap(True)
            styleEdit = StyleEdit(styleDesc, toolTip=styleDesc.description)
            styleEdit.styleChanged.connect(self.updatedStyle)
            self.styleEdits[styleDesc.key] = styleEdit
            formLayout.addRow(label, styleEdit)

        wrapper = QtWidgets.QWidget()
        wrapper.setLayout(formLayout)
        wrapper.setMinimumWidth(650)
        scrollArea.setWidget(wrapper)

        # Basic theme I/O

        curThemeLbl = QtWidgets.QLabel(text="Themes :")

        self.curThemeCmb = curThemeCmb = QtWidgets.QComboBox()
        current_index = -1
        for i, themeName in enumerate(self.themes.keys()):
            # We store the themeName in data in case the user renames one
            curThemeCmb.addItem(themeName, userData=themeName)
            if themeName == pyzo.config.settings.theme.lower():
                current_index = i
        curThemeCmb.addItem("New...")

        loadLayout = QtWidgets.QHBoxLayout()
        loadLayout.addWidget(curThemeLbl)
        loadLayout.addWidget(curThemeCmb)

        self.saveBtn = saveBtn = QtWidgets.QPushButton(text="Save")
        saveBtn.clicked.connect(self.saveTheme)
        exitBtn = QtWidgets.QPushButton(text="Apply theme")
        exitBtn.clicked.connect(self.ok)

        exitLayout = QtWidgets.QHBoxLayout()
        exitLayout.addWidget(exitBtn)
        exitLayout.addWidget(saveBtn)

        # Packing it up
        mainLayout = QtWidgets.QVBoxLayout()
        mainLayout.addLayout(loadLayout)
        mainLayout.addWidget(scrollArea)
        mainLayout.addLayout(exitLayout)
        self.setLayout(mainLayout)

        curThemeCmb.currentIndexChanged.connect(self.indexChanged)
        curThemeCmb.currentTextChanged.connect(self.setTheme)

        # Init
        if current_index >= 0:
            curThemeCmb.setCurrentIndex(current_index)
            self.setTheme(pyzo.config.settings.theme)

    def createTheme(self):
        """Create a new theme based on the current
        theme selected.
        """

        index = self.curThemeCmb.currentIndex()
        if index != self.curThemeCmb.count() - 1:
            return self.curThemeCmb.setCurrentIndex(self.curThemeCmb.count() -
                                                    1)

        # Select a new name
        t = "new_theme_x"
        i = 1
        themeName = t.replace("x", str(i))
        while themeName in self.themes:
            i += 1
            themeName = t.replace("x", str(i))

        # Create new theme
        new_theme = {"name": themeName, "data": {}, "builtin": False}
        if self.cur_theme:
            new_theme["data"] = self.cur_theme["data"].copy()
        self.cur_theme_key = themeName
        self.cur_theme = new_theme
        self.themes[themeName] = new_theme

        self.curThemeCmb.setItemText(index, themeName)
        self.curThemeCmb.setItemData(index, themeName)

        self.curThemeCmb.setEditable(True)
        self.curThemeCmb.lineEdit().setCursorPosition(0)
        self.curThemeCmb.lineEdit().selectAll()

        self.saveBtn.setEnabled(True)

        self.curThemeCmb.addItem("New...", )

    def setTheme(self, name):
        """Set the theme by its name. The combobox becomes editable only
        if the theme is not builtin. This method is connected to the signal
        self.curThemeCmb.currentTextChanged ; so it also filters
        parasites events"""

        name = name.lower()

        if name != self.curThemeCmb.currentText():
            # An item was added to the comboBox
            # But it's not a user action so we quit
            print(" -> Cancelled because this was not a user action")
            return

        if self.cur_theme_key == self.curThemeCmb.currentData():
            # The user renamed an existing theme
            self.cur_theme["name"] = name
            return

        if name not in self.themes:
            return

        # Sets the curent theme key
        self.cur_theme_key = name
        self.cur_theme = self.themes[name]

        if self.cur_theme["builtin"]:
            self.saveBtn.setEnabled(False)
            self.saveBtn.setText("Cannot save builtin style")
        else:
            self.saveBtn.setEnabled(True)
            self.saveBtn.setText("Save")
        self.curThemeCmb.setEditable(not self.cur_theme["builtin"])

        for key, le in self.styleEdits.items():
            if key in self.cur_theme["data"]:
                try:
                    le.setStyle(self.cur_theme["data"][key])
                except Exception as e:
                    print("Exception while setting style", key, "for theme",
                          name, ":", e)

    def saveTheme(self):
        """Saves the current theme to the disk, in appDataDir/themes"""

        if self.cur_theme["builtin"]:
            return
        themeName = self.curThemeCmb.currentText().strip()
        if not themeName:
            return

        # Get user theme dir and make sure it exists
        dir = os.path.join(pyzo.appDataDir, "themes")
        os.makedirs(dir, exist_ok=True)

        # Try to delete the old file if it exists (useful if it was renamed)
        try:
            os.remove(os.path.join(dir, self.cur_theme_key + ".theme"))
        except Exception:
            pass

        # This is the needed because of the SSDF format:
        # it doesn't accept dots, so we put underscore instead
        data = {
            x.replace(".", "_"): y
            for x, y in self.cur_theme["data"].items()
        }

        fname = os.path.join(dir, themeName + ".theme")
        ssdf.save(fname, {"name": themeName, "data": data})
        print("Saved theme '%s' to '%s'" % (themeName, fname))

    def ok(self):
        """On user click saves the cur_theme if modified
        and restart pyzo if the theme changed"""
        prev = pyzo.config.settings.theme
        new = self.cur_theme["name"]

        self.saveTheme()

        if prev != new:
            pyzo.config.settings.theme = new
            # This may be better
            pyzo.main.restart()
        else:
            self.done.emit(1)

    def indexChanged(self, index):
        # User selected the "new..." button
        if index == self.curThemeCmb.count() - 1:
            self.createTheme()

    def focusOnStyle(self, key):
        self.styleEdits[key].setFocus(True)
        self.scrollArea.ensureWidgetVisible(self.styleEdits[key])

    def updatedStyle(self, style, text):
        fmt = StyleFormat(self.cur_theme["data"][style])
        fmt.update(text)
        self.cur_theme["data"][style] = str(fmt)
        self.styleChanged.emit({style: text})
Beispiel #29
0
 def postEventWithCallback(self, callback, *args):
     self.queue.put((callback, args))
     QtWidgets.qApp.postEvent(self, QtCore.QEvent(QtCore.QEvent.User))
Beispiel #30
0
class ToolManager(QtCore.QObject):
    """Manages the tools."""

    # This signal indicates a change in the loaded tools
    toolInstanceChange = QtCore.Signal()

    def __init__(self, parent=None):
        QtCore.QObject.__init__(self, parent)

        # list of ToolDescription instances
        self._toolInfo = None
        self._activeTools = {}

    def loadToolInfo(self):
        """(re)load the tool information."""
        # Get paths to load files from
        toolDir1 = os.path.join(pyzo.pyzoDir, "tools")
        toolDir2 = os.path.join(pyzo.appDataDir, "tools")

        # Create list of tool files
        toolfiles = []
        for toolDir in [toolDir1, toolDir2]:
            tmp = [os.path.join(toolDir, f) for f in os.listdir(toolDir)]
            toolfiles.extend(tmp)

        # Note: we do not use the code below anymore, since even the frozen
        # app makes use of the .py files.
        #         # Get list of files, also when we're in a zip file.
        #         i = tooldir.find('.zip')
        #         if i>0:
        #             # Get list of files from zipfile
        #             tooldir = tooldir[:i+4]
        #             import zipfile
        #             z = zipfile.ZipFile(tooldir)
        #             toolfiles = [os.path.split(i)[1] for i in z.namelist()
        #                         if i.startswith('visvis') and i.count('functions')]
        #         else:
        #             # Get list of files from file system
        #             toolfiles = os.listdir(tooldir)

        # Iterate over tool modules
        newlist = []
        for file in toolfiles:
            modulePath = file

            # Check
            if os.path.isdir(file):
                file = os.path.join(file, "__init__.py")  # A package perhaps
                if not os.path.isfile(file):
                    continue
            elif file.endswith("__.py") or not file.endswith(".py"):
                continue
            elif file.endswith("pyzoFileBrowser.py"):
                # Skip old file browser (the file can be there from a previous install)
                continue

            #
            toolName = ""
            toolSummary = ""
            # read file to find name or summary
            linecount = 0
            for line in open(file, encoding="utf-8"):
                linecount += 1
                if linecount > 50:
                    break
                if line.startswith("tool_name"):
                    i = line.find("=")
                    if i < 0:
                        continue
                    line = line.rstrip("\n").rstrip("\r")
                    line = line[i + 1:].strip(" ")
                    toolName = eval(line)  # applies translation
                elif line.startswith("tool_summary"):
                    i = line.find("=")
                    if i < 0:
                        continue
                    line = line.rstrip("\n").rstrip("\r")
                    line = line[i + 1:].strip(" ")
                    toolSummary = line.strip("'").strip('"')
                else:
                    pass

            # Add stuff
            tmp = ToolDescription(modulePath, toolName, toolSummary)
            newlist.append(tmp)

        # Store and return
        self._toolInfo = sorted(newlist, key=lambda x: x.id)
        self.updateToolInstances()
        return self._toolInfo

    def updateToolInstances(self):
        """Make tool instances up to date, so that it can be seen what
        tools are now active."""
        for toolDes in self.getToolInfo():
            if toolDes.id in self._activeTools:
                toolDes.instance = self._activeTools[toolDes.id]
            else:
                toolDes.instance = None

        # Emit update signal
        self.toolInstanceChange.emit()

    def getToolInfo(self):
        """Like loadToolInfo(), but use buffered instance if available."""
        if self._toolInfo is None:
            self.loadToolInfo()
        return self._toolInfo

    def getToolClass(self, toolId):
        """Get the class of the tool.
        It will import (and reload) the module and get the class.
        Some checks are performed, like whether the class inherits
        from QWidget.
        Returns the class or None if failed...
        """

        # Make sure we have the info
        if self._toolInfo is None:
            self.loadToolInfo()

        # Get module name and path
        for toolDes in self._toolInfo:
            if toolDes.id == toolId:
                moduleName = toolDes.moduleName
                modulePath = toolDes.modulePath
                break
        else:
            print("WARNING: could not find module for tool", repr(toolId))
            return None

        # Remove from sys.modules, to force the module to reload
        for key in [key for key in sys.modules]:
            if key and key.startswith("pyzo.tools." + moduleName):
                del sys.modules[key]

        # Load module
        try:
            m_file, m_fname, m_des = imp.find_module(
                moduleName, [os.path.dirname(modulePath)])
            mod = imp.load_module("pyzo.tools." + moduleName, m_file, m_fname,
                                  m_des)
        except Exception as why:
            print("Invalid tool " + toolId + ":", why)
            return None

        # Is the expected class present?
        className = ""
        for member in dir(mod):
            if member.lower() == toolId:
                className = member
                break
        else:
            print("Invalid tool, Classname must match module name '%s'!" %
                  toolId)
            return None

        # Does it inherit from QWidget?
        plug = mod.__dict__[className]
        if not (isinstance(plug, type)
                and issubclass(plug, QtWidgets.QWidget)):
            print("Invalid tool, tool class must inherit from QWidget!")
            return None

        # Succes!
        return plug

    def loadTool(self, toolId, splitWith=None):
        """Load a tool by creating a dock widget containing the tool widget."""

        # A tool id should always be lower case
        toolId = toolId.lower()

        # Close old one
        if toolId in self._activeTools:
            old = self._activeTools[toolId].widget()
            self._activeTools[toolId].setWidget(QtWidgets.QWidget(pyzo.main))
            if old:
                old.close()
                old.deleteLater()

        # Get tool class (returns None on failure)
        toolClass = self.getToolClass(toolId)
        if toolClass is None:
            return

        # Already loaded? reload!
        if toolId in self._activeTools:
            self._activeTools[toolId].reload(toolClass)
            return

        # Obtain name from buffered list of names
        for toolDes in self._toolInfo:
            if toolDes.id == toolId:
                name = toolDes.name
                break
        else:
            name = toolId

        # Make sure there is a config entry for this tool
        if not hasattr(pyzo.config.tools, toolId):
            pyzo.config.tools[toolId] = ssdf.new()

        # Create dock widget and add in the main window
        dock = ToolDockWidget(pyzo.main, self)
        dock.setTool(toolId, name, toolClass)

        if splitWith and splitWith in self._activeTools:
            otherDock = self._activeTools[splitWith]
            pyzo.main.splitDockWidget(otherDock, dock, QtCore.Qt.Horizontal)
        else:
            pyzo.main.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)

        # Add to list
        self._activeTools[toolId] = dock
        self.updateToolInstances()

    def reloadTools(self):
        """Reload all tools."""
        for id in self.getLoadedTools():
            self.loadTool(id)

    def closeTool(self, toolId):
        """Close the tool with specified id."""
        if toolId in self._activeTools:
            dock = self._activeTools[toolId]
            dock.close()

    def getTool(self, toolId):
        """Get the tool widget instance, or None
        if not available."""
        if toolId in self._activeTools:
            return self._activeTools[toolId].widget()
        else:
            return None

    def onToolClose(self, toolId):
        # Remove from dict
        self._activeTools.pop(toolId, None)
        # Set instance to None
        self.updateToolInstances()

    def getLoadedTools(self):
        """Get a list with id's of loaded tools."""
        tmp = []
        for toolDes in self.getToolInfo():
            if toolDes.id in self._activeTools:
                tmp.append(toolDes.id)
        return tmp