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))
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
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())
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)))
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
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))
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)
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 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_()
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)
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
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
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)
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)
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)
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 __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)
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()
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)()
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()
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
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()
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
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)
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
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
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")
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})
def postEventWithCallback(self, callback, *args): self.queue.put((callback, args)) QtWidgets.qApp.postEvent(self, QtCore.QEvent(QtCore.QEvent.User))
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