class CompactTabBar(QtGui.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): QtGui.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 = QtGui.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(QtGui.QTabBar.tabText(self, i)) tabData = TabData(name) QtGui.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 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): QtGui.QTabBar.tabInserted(self, i) # Is called when a tab is inserted # Get given name and store name = str(QtGui.QTabBar.tabText(self, i)) tabData = TabData(name) QtGui.QTabBar.setTabData(self, i, tabData) # Update self.alignTabs() def tabRemoved(self, i): QtGui.QTabBar.tabRemoved(self, i) # Update self.alignTabs() def resizeEvent(self, event): QtGui.QTabBar.resizeEvent(self, event) self.alignTabs() def showEvent(self, event): QtGui.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(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. """ # Prepare for measuring font sizes font = self.font() metrics = QtGui.QFontMetrics(font) # Get whether an item was reduced in size itemReduced = False for i in range(self.count()): # Get width w = self._alignWidth # Get name name = name0 = self._compactTabBarData(i).name # Check if we can reduce the name size, correct w if necessary if ((w + 1) < len(name0)) 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(name0): 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(name0): name = name[:w] + ELLIPSIS itemReduced = True # Set text now QtGui.QTabBar.setTabText(self, i, name) # Done return itemReduced
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 iep.shells.currentShellChanged.connect(self.onCurrentShellChanged) iep.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 = iep.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 = iep.shells.getCurrentShell() if not shell: self._variables = [] self.haveNewData.emit() def onCurrentShellStateChanged(self): """ onCurrentShellStateChanged() Do a request for information! """ shell = iep.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()
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(iep.iepDir, 'tools') toolDir2 = os.path.join(iep.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('iepFileBrowser.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 = line.strip("'").strip('"') 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('ieptools.' + 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('ieptools.' + 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, QtGui.QWidget)): print("Invalid tool, tool class must inherit from QWidget!") return None # Succes! return plug def loadTool(self, toolId): """ 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(QtGui.QWidget(iep.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(iep.config.tools, toolId): iep.config.tools[toolId] = ssdf.new() # Create dock widget and add in the main window dock = ToolDockWidget(iep.main, self) dock.setTool(toolId, name, toolClass) iep.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
class WebView(QtGui.QTextBrowser): """ Inherit the webview class to implement zooming using the mouse wheel. """ loadStarted = QtCore.Signal() loadFinished = QtCore.Signal(bool) def __init__(self, parent): QtGui.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 & QtGui.qApp.keyboardModifiers(): self.parent().wheelEvent(event) else: QtGui.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
class PythonShell(BaseShell): """ The PythonShell class implements the python part of the shell by connecting to a remote process that runs a Python interpreter. """ # Emits when the status string has changed or when receiving a new prompt stateChanged = QtCore.Signal(BaseShell) # Emits when the debug status is changed debugStateChanged = QtCore.Signal(BaseShell) def __init__(self, parent, info): BaseShell.__init__(self, parent) # Get standard info if not given. if info is None and iep.config.shellConfigs2: info = iep.config.shellConfigs2[0] if not info: info = KernelInfo(None) # Store info so we can reuse it on a restart self._info = info # For the editor to keep track of attempted imports self._importAttempts = [] # To keep track of the response for introspection self._currentCTO = None self._currentACO = None # Write buffer to store messages in for writing self._write_buffer = None # Create timer to keep polling any results # todo: Maybe use yoton events to process messages as they arrive. # I tried this briefly, but it seemd to be less efficient because # messages are not so much bach-processed anymore. We should decide # on either method. self._timer = QtCore.QTimer(self) self._timer.setInterval(POLL_TIMER_INTERVAL) # ms self._timer.setSingleShot(False) self._timer.timeout.connect(self.poll) self._timer.start() # Add context menu self._menu = ShellContextMenu(shell=self, parent=self) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(lambda p: self._menu.popup( self.mapToGlobal(p + QtCore.QPoint(0, 3)))) # Start! self.resetVariables() self.connectToKernel(info) def resetVariables(self): """ Resets some variables. """ # Reset read state self.setReadOnly(False) # Variables to store state, python version, builtins and keywords self._state = '' self._debugState = {} self._version = "" self._builtins = [] self._keywords = [] self._startup_info = {} self._start_time = 0 # (re)set import attempts self._importAttempts[:] = [] # Update self.stateChanged.emit(self) def connectToKernel(self, info): """ connectToKernel() Create kernel and connect to it. """ # Create yoton context self._context = ct = yoton.Context() # Create stream channels self._strm_out = yoton.SubChannel(ct, 'strm-out') self._strm_err = yoton.SubChannel(ct, 'strm-err') self._strm_raw = yoton.SubChannel(ct, 'strm-raw') self._strm_echo = yoton.SubChannel(ct, 'strm-echo') self._strm_prompt = yoton.SubChannel(ct, 'strm-prompt') self._strm_broker = yoton.SubChannel(ct, 'strm-broker') self._strm_action = yoton.SubChannel(ct, 'strm-action', yoton.OBJECT) # Set channels to sync mode. This means that if the IEP cannot process # the messages fast enough, the sending side is blocked for a short # while. We don't want our users to miss any messages. for c in [self._strm_out, self._strm_err]: c.set_sync_mode(True) # Create control channels self._ctrl_command = yoton.PubChannel(ct, 'ctrl-command') self._ctrl_code = yoton.PubChannel(ct, 'ctrl-code', yoton.OBJECT) self._ctrl_broker = yoton.PubChannel(ct, 'ctrl-broker') # Create status channels self._stat_interpreter = yoton.StateChannel(ct, 'stat-interpreter') self._stat_debug = yoton.StateChannel(ct, 'stat-debug', yoton.OBJECT) self._stat_startup = yoton.StateChannel(ct, 'stat-startup', yoton.OBJECT) self._stat_startup.received.bind(self._onReceivedStartupInfo) # Create introspection request channel self._request = yoton.ReqChannel(ct, 'reqp-introspect') # Connect! The broker will only start the kernel AFTER # we connect, so we do not miss out on anything. slot = iep.localKernelManager.createKernel(finishKernelInfo(info)) self._brokerConnection = ct.connect('localhost:%i' % slot) self._brokerConnection.closed.bind(self._onConnectionClose) # todo: see polling vs events # # Detect incoming messages # for c in [self._strm_out, self._strm_err, self._strm_raw, # self._strm_echo, self._strm_prompt, self._strm_broker, # self._strm_action, # self._stat_interpreter, self._stat_debug]: # c.received.bind(self.poll) def _onReceivedStartupInfo(self, channel): startup_info = channel.recv() # Store the whole dict self._startup_info = startup_info # Store when we received this self._start_time = time.time() # Set version version = startup_info.get('version', None) if isinstance(version, tuple): version = [str(v) for v in version] self._version = '.'.join(version[:2]) # Set keywords L = startup_info.get('keywords', None) if isinstance(L, list): self._keywords = L # Set builtins L = startup_info.get('builtins', None) if isinstance(L, list): self._builtins = L # Notify self.stateChanged.emit(self) ## Introspection processing methods def processCallTip(self, cto): """ Processes a calltip request using a CallTipObject instance. """ # Try using buffer first (not if we're not the requester) if self is cto.textCtrl: if cto.tryUsingBuffer(): return # Clear buffer to prevent doing a second request # and store cto to see whether the response is still wanted. cto.setBuffer('') self._currentCTO = cto # Post request future = self._request.signature(cto.name) future.add_done_callback(self._processCallTip_response) future.cto = cto def _processCallTip_response(self, future): """ Process response of shell to show signature. """ # Process future if future.cancelled(): #print('Introspect cancelled') # No kernel return elif future.exception(): print('Introspect-exception: ', future.exception()) return else: response = future.result() cto = future.cto # First see if this is still the right editor (can also be a shell) editor1 = iep.editors.getCurrentEditor() editor2 = iep.shells.getCurrentShell() if cto.textCtrl not in [editor1, editor2]: # The editor or shell starting the autocomp is no longer active aco.textCtrl.autocompleteCancel() return # Invalid response if response is None: cto.textCtrl.autocompleteCancel() return # If still required, show tip, otherwise only store result if cto is self._currentCTO: cto.finish(response) else: cto.setBuffer(response) def processAutoComp(self, aco): """ Processes an autocomp request using an AutoCompObject instance. """ # Try using buffer first (not if we're not the requester) if self is aco.textCtrl: if aco.tryUsingBuffer(): return # Include builtins and keywords? if not aco.name: aco.addNames(self._builtins) if iep.config.settings.autoComplete_keywords: aco.addNames(self._keywords) # Set buffer to prevent doing a second request # and store aco to see whether the response is still wanted. aco.setBuffer() self._currentACO = aco # Post request future = self._request.dir(aco.name) future.add_done_callback(self._processAutoComp_response) future.aco = aco def _processAutoComp_response(self, future): """ Process the response of the shell for the auto completion. """ # Process future if future.cancelled(): #print('Introspect cancelled') # No living kernel return elif future.exception(): print('Introspect-exception: ', future.exception()) return else: response = future.result() aco = future.aco # First see if this is still the right editor (can also be a shell) editor1 = iep.editors.getCurrentEditor() editor2 = iep.shells.getCurrentShell() if aco.textCtrl not in [editor1, editor2]: # The editor or shell starting the autocomp is no longer active aco.textCtrl.autocompleteCancel() return # Add result to the list foundNames = [] if response is not None: foundNames = response aco.addNames(foundNames) # Process list if aco.name and not foundNames: # No names found for the requested name. This means # it does not exist, let's try to import it importNames, importLines = iep.parser.getFictiveImports(editor1) baseName = aco.nameInImportNames(importNames) if baseName: line = importLines[baseName].strip() if line not in self._importAttempts: # Do import self.processLine(line + ' # auto-import') self._importAttempts.append(line) # Wait a barely noticable time to increase the chances # That the import is complete when we repost the request. time.sleep(0.2) # To be sure, decrease the experiration date on the buffer aco.setBuffer(timeout=1) # Repost request future = self._request.signature(aco.name) future.add_done_callback(self._processAutoComp_response) future.aco = aco else: # If still required, show list, otherwise only store result if self._currentACO is aco: aco.finish() else: aco.setBuffer() ## Methods for executing code def executeCommand(self, text): """ executeCommand(text) Execute one-line command in the remote Python session. """ self._ctrl_command.send(text) def executeCode(self, text, fname, lineno=0, cellName=None): """ executeCode(text, fname, lineno, cellName=None) Execute (run) a large piece of code in the remote shell. text: the source code to execute filename: the file from which the source comes lineno: the first lineno of the text in the file, where 0 would be the first line of the file... The text (source code) is first pre-processed: - convert all line-endings to \n - remove all empty lines at the end - remove commented lines at the end - convert tabs to spaces - dedent so minimal indentation is zero """ # Convert tabs to spaces text = text.replace("\t", " " * 4) # Make sure there is always *some* text if not text: text = ' ' # Examine the text line by line... # - check for empty/commented lined at the end # - calculate minimal indentation lines = text.splitlines() lastLineOfCode = 0 minIndent = 99 for linenr in range(len(lines)): # Get line line = lines[linenr] # Check if empty (can be commented, but nothing more) tmp = line.split("#", 1)[0] # get part before first # if tmp.count(" ") == len(tmp): continue # empty line, proceed else: lastLineOfCode = linenr # Calculate indentation tmp = line.lstrip(" ") indent = len(line) - len(tmp) if indent < minIndent: minIndent = indent # Copy all proper lines to a new list, # remove minimal indentation, but only if we then would only remove # spaces (in the case of commented lines) lines2 = [] for linenr in range(lastLineOfCode + 1): line = lines[linenr] # Remove indentation, if line[:minIndent].count(" ") == minIndent: line = line[minIndent:] else: line = line.lstrip(" ") lines2.append(line) # Send message text = "\n".join(lines2) msg = { 'source': text, 'fname': fname, 'lineno': lineno, 'cellName': cellName } self._ctrl_code.send(msg) ## The polling methods and terminating methods def poll(self, channel=None): """ poll() To keep the shell up-to-date. Call this periodically. """ if self._write_buffer: # There is still data in the buffer sub, M = self._write_buffer else: # Check what subchannel has the latest message pending sub = yoton.select_sub_channel(self._strm_out, self._strm_err, self._strm_echo, self._strm_raw, self._strm_broker, self._strm_prompt) # Read messages from it if sub: M = sub.recv_selected() #M = [sub.recv()] # Slow version (for testing) # Optimization: handle backspaces on stack of messages if sub is self._strm_out: M = self._handleBackspacesOnList(M) # New prompt? if sub is self._strm_prompt: self.stateChanged.emit(self) # Write all pending messages that are later than any other message if sub: # Select messages to process N = 256 M, buffer = M[:N], M[N:] # Buffer the rest if buffer: self._write_buffer = sub, buffer else: self._write_buffer = None # Get how to deal with prompt prompt = 0 if sub is self._strm_echo: prompt = 1 elif sub is self._strm_prompt: prompt = 2 # Get color color = None if sub is self._strm_broker: color = '#000' elif sub is self._strm_raw: color = '#888888' # Halfway elif sub is self._strm_err: color = '#F00' # Write self.write(''.join(M), prompt, color) # Do any actions? action = self._strm_action.recv(False) if action: if action.startswith('open '): fname = action.split(' ', 1)[1] iep.editors.loadFile(fname) else: print('Unkown action: %s' % action) # Update status state = self._stat_interpreter.recv() if state != self._state: self._state = state self.stateChanged.emit(self) # Update debug status state = self._stat_debug.recv() if state != self._debugState: self._debugState = state self.debugStateChanged.emit(self) def interrupt(self): """ interrupt() Send a Keyboard interrupt signal to the main thread of the remote process. """ self._ctrl_broker.send('INT') def restart(self, scriptFile=None): """ restart(scriptFile=None) Terminate the shell, after which it is restarted. Args can be a filename, to execute as a script as soon as the shell is back up. """ # Get info info = finishKernelInfo(self._info, scriptFile) # Create message and send msg = 'RESTART\n' + ssdf.saves(info) self._ctrl_broker.send(msg) # Reset self.resetVariables() def terminate(self): """ terminate() Terminates the python process. It will first try gently, but if that does not work, the process shall be killed. To be notified of the termination, connect to the "terminated" signal of the shell. """ self._ctrl_broker.send('TERM') def closeShell(self): # do not call it close(); that is a reserved method. """ closeShell() Very simple. This closes the shell. If possible, we will first tell the broker to terminate the kernel. The broker will be cleaned up if there are no clients connected and if there is no active kernel. In a multi-user environment, we should thus be able to close the shell without killing the kernel. But in a closed 1-to-1 environment we really want to prevent loose brokers and kernels dangling around. In both cases however, it is the responsibility of the broker to terminate the kernel, and the shell will simply assume that this will work :) """ # If we can, try to tell the broker to terminate the kernel if self._context and self._context.connection_count: self.terminate() self._context.flush() # Important, make sure the message is send! self._context.close() # Adios iep.shells.removeShell(self) def _onConnectionClose(self, c, why): """ To be called after disconnecting. In general, the broker will not close the connection, so it can be considered an error-state if this function is called. """ # Stop context if self._context: self._context.close() # New (empty prompt) self._cursor1.movePosition(self._cursor1.End, A_MOVE) self._cursor2.movePosition(self._cursor2.End, A_MOVE) self.write('\n\n') self.write('Lost connection with broker:\n') self.write(why) self.write('\n\n') # Set style to indicate dead-ness self.setReadOnly(True) # Goto end such that the closing message is visible cursor = self.textCursor() cursor.movePosition(cursor.End, A_MOVE) self.setTextCursor(cursor) self.ensureCursorVisible()
class ShellStackWidget(QtGui.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): QtGui.QWidget.__init__(self, parent) # create toolbar self._toolbar = QtGui.QToolBar(self) self._toolbar.setMaximumHeight(25) self._toolbar.setIconSize(QtCore.QSize(16,16)) # create stack self._stack = QtGui.QStackedWidget(self) # Populate toolbar self._shellButton = ShellControl(self._toolbar, self._stack) self._dbc = DebugControl(self._toolbar) # self._toolbar.addWidget(self._shellButton) self._toolbar.addSeparator() # self._toolbar.addWidget(self._dbc) -> delayed, see addContextMenu() # widget layout layout = QtGui.QVBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._toolbar) layout.addWidget(self._stack) self.setLayout(layout) # make callbacks self._stack.currentChanged.connect(self.onCurrentChanged) def __iter__(self): i = 0 while i < self._stack.count(): w = self._stack.widget(i) i += 1 yield w def addShell(self, shellInfo=None): """ addShell() Add a shell to the widget. """ # Create shell and add to stack shell = PythonShell(self, shellInfo) index = 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() 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']: iep.main.setWindowIcon(iep.iconRunning) else: iep.main.setWindowIcon(iep.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: self._dbc.setTrace(shell._debugState) else: self._dbc.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 iep.main.menuBar()._menumap['shell']._shellActions: action = self._toolbar.addAction(action) # Delayed-add debug control button self._toolbar.addWidget(self._dbc) 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)()
class IepEditor(BaseTextCtrl): # called when dirty changed or filename changed, etc somethingChanged = QtCore.Signal() def __init__(self, parent, **kwds): super().__init__(parent, showLineNumbers=True, **kwds) # Init filename and name self._filename = '' self._name = '<TMP>' # View settings self.setShowWhitespace(iep.config.view.showWhitespace) #TODO: self.setViewWrapSymbols(view.showWrapSymbols) self.setShowLineEndings(iep.config.view.showLineEndings) self.setShowIndentationGuides(iep.config.view.showIndentationGuides) # self.setWrap(bool(iep.config.view.wrap)) self.setHighlightCurrentLine(iep.config.view.highlightCurrentLine) self.setLongLineIndicatorPosition(iep.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 = iep.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))) ## Properties @property def name(self): return self._name @property def filename(self): return self._filename @property def lineEndings(self): """ Line-endings style of this file. Setter accepts machine-readable (e.g. '\r') and human-readable (e.g. 'CR') input """ return self._lineEndings @lineEndings.setter def lineEndings(self, value): if value in ('\r', '\n', '\r\n'): self._lineEndings = value return try: self._lineEndings = {'CR': '\r', 'LF': '\n', 'CRLF': '\r\n'}[value] except KeyError: raise ValueError('Invalid line endings style %r' % value) @property def lineEndingsHumanReadable(self): """ Current line-endings style, human readable (e.g. 'CR') """ return {'\r': 'CR', '\n': 'LF', '\r\n': 'CRLF'}[self.lineEndings] @property def encoding(self): """ Encoding used to convert the text of this file to bytes. """ return self._encoding @encoding.setter def encoding(self, value): # Test given value, correct name if it exists try: c = codecs.lookup(value) value = c.name except Exception: value = codecs.lookup('UTF-8').name # Store self._encoding = value ## def showRunCursor(self, cursor): """ Momentarily highlight a piece of code to show that this is being executed """ extraSelection = QtGui.QTextEdit.ExtraSelection() extraSelection.cursor = cursor extraSelection.format.setBackground(QtCore.Qt.gray) self.setExtraSelections([extraSelection]) self._showRunCursorTimer.singleShot( 200, lambda: self.setExtraSelections([])) def id(self): """ Get an id of this editor. This is the filename, or for tmp files, the name. """ if self._filename: return self._filename else: return self._name def focusInEvent(self, event): """ Test whether the file has been changed 'behind our back' """ # Act normally to the focus event BaseTextCtrl.focusInEvent(self, event) # Test file change self.testWhetherFileWasChanged() def testWhetherFileWasChanged(self): """ testWhetherFileWasChanged() Test to see whether the file was changed outside our backs, and let the user decide what to do. Returns True if it was changed. """ # get the path path = self._filename if not os.path.isfile(path): # file is deleted from the outside return # test the modification time... mtime = os.path.getmtime(path) if mtime != self._modifyTime: # ask user dlg = QtGui.QMessageBox(self) dlg.setWindowTitle('File was changed') dlg.setText("File has been modified outside of the editor:\n" + self._filename) dlg.setInformativeText("Do you want to reload?") t = dlg.addButton("Reload", QtGui.QMessageBox.AcceptRole) #0 dlg.addButton("Keep this version", QtGui.QMessageBox.RejectRole) #1 dlg.setDefaultButton(t) # whatever the result, we will reset the modified time self._modifyTime = os.path.getmtime(path) # get result and act result = dlg.exec_() if result == QtGui.QMessageBox.AcceptRole: self.reload() else: pass # when cancelled or explicitly said, do nothing # Return that indeed the file was changes return True def _onModificationChanged(self, changed): """Handler for the modificationChanged signal. Emit somethingChanged for the editorStack to update the modification notice.""" self.somethingChanged.emit() def _onModified(self): iep.parser.parseThis(self) def dropEvent(self, event): """ Drop files in the list. """ if event.mimeData().hasUrls(): # file: let the editorstack do the work. iep.editors.dropEvent(event) else: # text: act normal BaseTextCtrl.dropEvent(self, event) def showEvent(self, event=None): """ Capture show event to change title. """ # Act normally if event: BaseTextCtrl.showEvent(self, event) # Make parser update iep.parser.parseThis(self) def setTitleInMainWindow(self): """ set the title text in the main window to show filename. """ # compose title name, path = self._name, self._filename if not path: path = 'no location on disk' tmp = { 'fileName': name, 'filename': name, 'name': name, 'fullPath': path, 'fullpath': path, 'path': path } title = iep.config.advanced.titleText.format(**tmp) # set title iep.main.setWindowTitle(title) def save(self, filename=None): """ Save the file. No checking is done. """ # get filename if filename is None: filename = self._filename if not filename: raise ValueError("No filename specified, and no filename known.") # Test whether it was changed without us knowing. If so, dont save now. if self.testWhetherFileWasChanged(): return # Get text, convert line endings text = self.toPlainText() text = text.replace('\n', self.lineEndings) # Make bytes bb = text.encode(self.encoding) # Store f = open(filename, 'wb') try: f.write(bb) finally: f.close() # Update stats self._filename = normalizePath(filename) self._name = os.path.split(self._filename)[1] self.document().setModified(False) self._modifyTime = os.path.getmtime(self._filename) # update title (in case of a rename) self.setTitleInMainWindow() # allow item to update its texts (no need: onModifiedChanged does this) #self.somethingChanged.emit() def reload(self): """ Reload text using the self._filename. We do not have a load method; we first try to load the file and only when we succeed create an editor to show it in... This method is only for reloading in case the file was changed outside of the editor. """ # We can only load if the filename is known if not self._filename: return filename = self._filename # Remember where we are cursor = self.textCursor() linenr = cursor.blockNumber() + 1 index = cursor.positionInBlock() # Load file (as bytes) with open(filename, 'rb') as f: bb = f.read() # Convert to text text = bb.decode('UTF-8') # Process line endings (before setting the text) self.lineEndings = determineLineEnding(text) # Set text self.setPlainText(text) self.document().setModified(False) # Go where we were (approximately) self.gotoLine(linenr) def deleteLines(self): cursor = self.textCursor() # Find start and end of selection start = cursor.selectionStart() end = cursor.selectionEnd() # Expand selection: from start of first block to start of next block cursor.setPosition(start) cursor.movePosition(cursor.StartOfBlock) cursor.setPosition(end, cursor.KeepAnchor) cursor.movePosition(cursor.NextBlock, cursor.KeepAnchor) cursor.removeSelectedText() def commentCode(self): """ Comment the lines that are currently selected """ self.doForSelectedBlocks(lambda cursor: cursor.insertText('# ')) def uncommentCode(self): """ Uncomment the lines that are currently selected """ #TODO: this should not be applied to lines that are part of a multi-line string #Define the uncomment function to be applied to all blocks def uncommentBlock(cursor): """ Find the first # on the line; if there is just whitespace before it, remove the # and if it is followed by a space remove the space, too """ text = cursor.block().text() commentStart = text.find('#') if commentStart == -1: return #No comment on this line if text[:commentStart].strip() != '': return #Text before the # #Move the cursor to the beginning of the comment cursor.setPosition(cursor.block().position() + commentStart) cursor.deleteChar() if text[commentStart:].startswith('# '): cursor.deleteChar() #Apply this function to all blocks self.doForSelectedBlocks(uncommentBlock) ## Introspection processing methods def processCallTip(self, cto): """ Processes a calltip request using a CallTipObject instance. """ # Try using buffer first if cto.tryUsingBuffer(): return # Try obtaining calltip from the source sig = iep.parser.getFictiveSignature(cto.name, self, True) if sig: # Done cto.finish(sig) else: # Try the shell shell = iep.shells.getCurrentShell() if shell: shell.processCallTip(cto) def processAutoComp(self, aco): """ Processes an autocomp request using an AutoCompObject instance. """ # Try using buffer first if aco.tryUsingBuffer(): return # Init name to poll by remote process (can be changed!) nameForShell = aco.name # Get normal fictive namespace fictiveNS = iep.parser.getFictiveNameSpace(self) fictiveNS = set(fictiveNS) # Add names if not aco.name: # "root" names aco.addNames(fictiveNS) # imports importNames, importLines = iep.parser.getFictiveImports(self) aco.addNames(importNames) else: # Prepare list of class names to check out classNames = [aco.name] handleSelf = True # Unroll supers while classNames: className = classNames.pop(0) if not className: continue if handleSelf or (className in fictiveNS): # Only the self list (only first iter) fictiveClass = iep.parser.getFictiveClass( className, self, handleSelf) handleSelf = False if fictiveClass: aco.addNames(fictiveClass.members) classNames.extend(fictiveClass.supers) else: nameForShell = className break # If there's a shell, let it finish the autocompletion shell = iep.shells.getCurrentShell() if shell: aco.name = nameForShell # might be the same or a base class shell.processAutoComp(aco) else: # Otherwise we finish it ourselves aco.finish()
class EditorTabs(QtGui.QWidget): """ The EditorTabs instance manages the open files and corresponding editors. It does the saving loading etc. """ # Signal to notify that a different file was selected currentChanged = QtCore.Signal() # Signal to notify that the parser has parsed the text (emit by parser) parserDone = QtCore.Signal() def __init__(self, parent): QtGui.QWidget.__init__(self,parent) # keep a booking of opened directories self._lastpath = '' # create tab widget self._tabs = FileTabWidget(self) self._tabs.tabCloseRequested.connect(self.closeFile) self._tabs.currentChanged.connect(self.onCurrentChanged) # Double clicking a tab saves the file, clicking on the bar opens a new file self._tabs.tabBar().tabDoubleClicked.connect(self.saveFile) self._tabs.tabBar().barDoubleClicked.connect(self.newFile) # Create find/replace widget self._findReplace = FindReplaceWidget(self) # create box layout control and add widgets self._boxLayout = QtGui.QVBoxLayout(self) self._boxLayout.addWidget(self._tabs, 1) self._boxLayout.addWidget(self._findReplace, 0) # spacing of widgets self._boxLayout.setSpacing(0) # apply self.setLayout(self._boxLayout) #self.setAttribute(QtCore.Qt.WA_AlwaysShowToolTips,True) # accept drops self.setAcceptDrops(True) # restore state (call later so that the menu module can bind to the # currentChanged signal first, in order to set tab/indentation # checkmarks appropriately) # todo: Resetting the scrolling would work better if set after # the widgets are properly sized. iep.callLater(self.restoreEditorState) def addContextMenu(self): """ Adds a context menu to the tab bar """ from iep.iepcore.menu import EditorTabContextMenu self._menu = EditorTabContextMenu(self, "EditorTabMenu") self._tabs.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self._tabs.customContextMenuRequested.connect(self.contextMenuTriggered) def contextMenuTriggered(self, p): """ Called when context menu is clicked """ # Get index of current tab index = self._tabs.tabBar().tabAt(p) self._menu.setIndex(index) # Show menu if item is available if index >= 0: p = self._tabs.tabBar().tabRect(index).bottomLeft() self._menu.popup(self._tabs.tabBar().mapToGlobal(p)) def onCurrentChanged(self): self.currentChanged.emit() def getCurrentEditor(self): """ Get the currently active editor. """ item = self._tabs.currentItem() if item: return item.editor else: return None def getMainEditor(self): """ Get the editor that represents the main file, or None if there is no main file. """ item = self._tabs.mainItem() if item: return item.editor else: return None def __iter__(self): tmp = [item.editor for item in self._tabs.items()] return tmp.__iter__() ## Loading ad saving files def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): """ Drop files in the list. """ for qurl in event.mimeData().urls(): path = str( qurl.toLocalFile() ) if os.path.isfile(path): self.loadFile(path) elif os.path.isdir(path): self.loadDir(path) else: pass def newFile(self): """ Create a new (unsaved) file. """ # create editor editor = createEditor(self, None) # add to list item = FileItem(editor) self._tabs.addItem(item) self._tabs.setCurrentItem(item) # set focus to new file editor.setFocus() return item def openFile(self): """ Create a dialog for the user to select a file. """ # determine start dir # todo: better selection of dir, using project manager editor = self.getCurrentEditor() if editor and editor._filename: startdir = os.path.split(editor._filename)[0] else: startdir = self._lastpath if not os.path.isdir(startdir): startdir = '' # show dialog msg = "Select one or more files to open" filter = "Python (*.py *.pyw);;" filter += "Pyrex (*.pyi *.pyx *.pxd);;" filter += "C (*.c *.h *.cpp *.c++);;" #filter += "Py+Cy+C (*.py *.pyw *.pyi *.pyx *.pxd *.c *.h *.cpp);;" filter += "All (*)" if True: filenames = QtGui.QFileDialog.getOpenFileNames(self, msg, startdir, filter) if isinstance(filenames, tuple): # PySide filenames = filenames[0] else: # Example how to preselect files, can be used when the users # opens a file in a project to select all files currently not # loaded. d = QtGui.QFileDialog(self, msg, startdir, filter) d.setFileMode(d.ExistingFiles) d.selectFile('"codeparser.py" "editorStack.py"') d.exec_() if d.result(): filenames = d.selectedFiles() else: filenames = [] # were some selected? if not filenames: return # load for filename in filenames: self.loadFile(filename) def openDir(self): """ Create a dialog for the user to select a directory. """ # determine start dir editor = self.getCurrentEditor() if editor and editor._filename: startdir = os.path.split(editor._filename)[0] else: startdir = self._lastpath if not os.path.isdir(startdir): startdir = '' # show dialog msg = "Select a directory to open" dirname = QtGui.QFileDialog.getExistingDirectory(self, msg, startdir) # was a dir selected? if not dirname: return # load self.loadDir(dirname) def loadFile(self, filename, updateTabs=True): """ Load the specified file. On success returns the item of the file, also if it was already open.""" # Note that by giving the name of a tempfile, we can select that # temp file. # normalize path if filename[0] != '<': filename = normalizePath(filename) if not filename: return None # if the file is already open... for item in self._tabs.items(): if item.id == filename: # id gets _filename or _name for temp files break else: item = None if item: self._tabs.setCurrentItem(item) print("File already open: '{}'".format(filename)) return item # create editor try: editor = createEditor(self, filename) except Exception as err: # Notify in logger print("Error loading file: ", err) # Make sure the user knows m = QtGui.QMessageBox(self) m.setWindowTitle("Error loading file") m.setText(str(err)) m.setIcon(m.Warning) m.exec_() return None # create list item item = FileItem(editor) self._tabs.addItem(item, updateTabs) if updateTabs: self._tabs.setCurrentItem(item) # store the path self._lastpath = os.path.dirname(item.filename) return item def loadDir(self, path): """ Create a project with the dir's name and add all files contained in the directory to it. extensions is a komma separated list of extenstions of files to accept... """ # if the path does not exist, stop path = os.path.abspath(path) if not os.path.isdir(path): print("ERROR loading dir: the specified directory does not exist!") return # get extensions extensions = iep.config.advanced.fileExtensionsToLoadFromDir extensions = extensions.replace(',',' ').replace(';',' ') extensions = ["."+a.lstrip(".").strip() for a in extensions.split(" ")] # init item item = None # open all qualified files... self._tabs.setUpdatesEnabled(False) try: filelist = os.listdir(path) for filename in filelist: filename = os.path.join(path, filename) ext = os.path.splitext(filename)[1] if str(ext) in extensions: item = self.loadFile(filename, False) finally: self._tabs.setUpdatesEnabled(True) self._tabs.updateItems() # return lastopened item return item def saveFileAs(self, editor=None): """ Create a dialog for the user to select a file. returns: True if succesfull, False if fails """ # get editor if editor is None: editor = self.getCurrentEditor() if editor is None: return False # get startdir if editor._filename: startdir = os.path.dirname(editor._filename) else: startdir = self._lastpath # Try the file browser or project manager to suggest a path fileBrowser = iep.toolManager.getTool('iepfilebrowser') projectManager = iep.toolManager.getTool('iepprojectmanager') if fileBrowser: startdir = fileBrowser.getDefaultSavePath() if projectManager and not startdir: startdir = projectManager.getDefaultSavePath() if not os.path.isdir(startdir): startdir = '' # show dialog msg = "Select the file to save to" filter = "Python (*.py *.pyw);;" filter += "Pyrex (*.pyi *.pyx *.pxd);;" filter += "C (*.c *.h *.cpp);;" #filter += "Py+Cy+C (*.py *.pyw *.pyi *.pyx *.pxd *.c *.h *.cpp);;" filter += "All (*.*)" filename = QtGui.QFileDialog.getSaveFileName(self, msg, startdir, filter) if isinstance(filename, tuple): # PySide filename = filename[0] # give python extension if it has no extension head, tail = os.path.split(filename) if tail and '.' not in tail: filename += '.py' # proceed or cancel if filename: return self.saveFile(editor, filename) else: return False # Cancel was pressed def saveFile(self, editor=None, filename=None): """ Save the file. returns: True if succesfull, False if fails """ # get editor if editor is None: editor = self.getCurrentEditor() elif isinstance(editor, int): index = editor editor = None if index>=0: item = self._tabs.items()[index] editor = item.editor if editor is None: return False # get filename if filename is None: filename = editor._filename if not filename: return self.saveFileAs(editor) # let the editor do the low level stuff... try: editor.save(filename) except Exception as err: # Notify in logger print("Error saving file:",err) # Make sure the user knows m = QtGui.QMessageBox(self) m.setWindowTitle("Error saving file") m.setText(str(err)) m.setIcon(m.Warning) m.exec_() # Return now return False # get actual normalized filename filename = editor._filename # notify # TODO: message concerining line endings print("saved file: {} ({})".format(filename, editor.lineEndingsHumanReadable)) self._tabs.updateItems() # todo: this is where we once detected whether the file being saved was a style file. # Notify done return True def saveAllFiles(self): """ Save all files""" for editor in self: self.saveFile(editor) ## Closing files / closing down def askToSaveFileIfDirty(self, editor): """ askToSaveFileIfDirty(editor) If the given file is not saved, pop up a dialog where the user can save the file . Returns 1 if file need not be saved. Returns 2 if file was saved. Returns 3 if user discarded changes. Returns 0 if cancelled. """ # should we ask to save the file? if editor.document().isModified(): # Ask user what to do result = simpleDialog(editor, "Closing", "Save modified file?", ['Discard', 'Cancel', 'Save'], 'Save') result = result.lower() # Get result and act if result == 'save': return 2 if self.saveFile(editor) else 0 elif result == 'discard': return 3 else: # cancel return 0 return 1 def closeFile(self, editor=None): """ Close the selected (or current) editor. Returns same result as askToSaveFileIfDirty() """ # get editor if editor is None: editor = self.getCurrentEditor() item = self._tabs.currentItem() elif isinstance(editor, int): index = editor editor, item = None, None if index>=0: item = self._tabs.items()[index] editor = item.editor else: item = None for i in self._tabs.items(): if i.editor is editor: item = i if editor is None or item is None: return # Ask if dirty result = self.askToSaveFileIfDirty(editor) # Ask if closing pinned file if result and item.pinned: result = simpleDialog(editor, "Closing pinned", "Are you sure you want to close this pinned file?", ['Close', 'Cancel'], 'Cancel') result = result == 'Close' # ok, close... if result: if editor._name.startswith("<tmp"): # Temp file, try to find its index for i in range(len(self._tabs.items())): if self._tabs.getItemAt(i).editor is editor: self._tabs.removeTab(i) break else: self._tabs.removeTab(editor) return result def closeAllFiles(self): """Close all files""" for editor in self: self.closeFile(editor) def saveEditorState(self): """ Save the editor's state configuration. """ fr = self._findReplace iep.config.state.find_matchCase = fr._caseCheck.isChecked() iep.config.state.find_regExp = fr._regExp.isChecked() iep.config.state.find_wholeWord = fr._wholeWord.isChecked() iep.config.state.find_show = fr.isVisible() # iep.config.state.editorState2 = self._getCurrentOpenFilesAsSsdfList() def restoreEditorState(self): """ Restore the editor's state configuration. """ # Restore opened editors if iep.config.state.editorState2: self._setCurrentOpenFilesAsSsdfList(iep.config.state.editorState2) else: self.newFile() # The find/replace state is set in the corresponding class during init def _getCurrentOpenFilesAsSsdfList(self): """ Get the state as it currently is as an ssdf list. The state entails all open files and their structure in the projects. The being collapsed of projects and their main files. The position of the cursor in the editors. """ # Init state = [] # Get items for item in self._tabs.items(): # Get editor ed = item.editor if not ed._filename: continue # Init info info = [] # Add filename, line number, and scroll distance info.append(ed._filename) info.append(int(ed.textCursor().position())) info.append(int(ed.verticalScrollBar().value())) # Add whether pinned or main file if item.pinned: info.append('pinned') if item.id == self._tabs._mainFile: info.append('main') # Add to state state.append( tuple(info) ) # Get history history = [item for item in self._tabs._itemHistory] history.reverse() # Last one is current for item in history: if isinstance(item, FileItem): ed = item._editor if ed._filename: state.append( (ed._filename, 'hist') ) # Done return state def _setCurrentOpenFilesAsSsdfList(self, state): """ Set the state of the editor in terms of opened files. The input should be a list object as returned by ._getCurrentOpenFilesAsSsdfList(). """ # Init dict fileItems = {} # Process items for item in state: fname = item[0] if item[1] == 'hist': # select item (to make the history right) if fname in fileItems: self._tabs.setCurrentItem( fileItems[fname] ) elif fname: # a file item, create editor-item and store itm = self.loadFile(fname) fileItems[fname] = itm # set position if itm: try: ed = itm.editor cursor = ed.textCursor() cursor.setPosition(int(item[1])) ed.setTextCursor(cursor) # set scrolling ed.verticalScrollBar().setValue(int(item[2])) #ed.centerCursor() #TODO: this does not work properly yet # set main and/or pinned? if 'main' in item: self._tabs._mainFile = itm.id if 'pinned' in item: itm._pinned = True except Exception as err: print('Could not set position for %s' % fname, err) def closeAll(self): """ Close all files (well technically, we don't really close them, so that they are all stil there when the user presses cancel). Returns False if the user pressed cancel when asked for saving an unsaved file. """ # try closing all editors. for editor in self: result = self.askToSaveFileIfDirty(editor) if not result: return False # we're good to go closing return True