Skip to content

Commit

Permalink
Merge from 3.x: PR #4092
Browse files Browse the repository at this point in the history
Fixes #1962
  • Loading branch information
ccordoba12 committed Jul 2, 2017
2 parents 71f5f6b + c92bf8f commit fe8d342
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 31 deletions.
69 changes: 51 additions & 18 deletions spyder/plugins/ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ def __init__(self, parent, testing=False):
os.mkdir(programs.TEMPDIR)

layout = QVBoxLayout()
self.tabwidget = Tabs(self, self.menu_actions)
self.tabwidget = Tabs(self, self.menu_actions, rename_tabs=True)
if hasattr(self.tabwidget, 'setDocumentMode')\
and not sys.platform == 'darwin':
# Don't set document mode to true on OSX because it generates
Expand All @@ -626,6 +626,8 @@ def __init__(self, parent, testing=False):
self.tabwidget.setDocumentMode(True)
self.tabwidget.currentChanged.connect(self.refresh_plugin)
self.tabwidget.move_data.connect(self.move_tab)
self.tabwidget.tabBar().sig_change_name.connect(
self.rename_tabs_after_change)

self.tabwidget.set_close_function(self.close_client)

Expand Down Expand Up @@ -758,6 +760,10 @@ def get_plugin_actions(self):
_("Open a new IPython console connected to an existing kernel"),
triggered=self.create_client_for_kernel)

rename_tab_action = create_action(self, _("Rename tab"),
icon=ima.icon('rename'),
triggered=self.tab_name_editor)

# Add the action to the 'Consoles' menu on the main window
main_consoles_menu = self.main.consoles_menu_actions
main_consoles_menu.insert(0, create_client_action)
Expand All @@ -766,7 +772,8 @@ def get_plugin_actions(self):

# Plugin actions
self.menu_actions = [create_client_action, MENU_SEPARATOR,
restart_action, connect_to_kernel_action]
restart_action, connect_to_kernel_action,
MENU_SEPARATOR, rename_tab_action]

return self.menu_actions

Expand Down Expand Up @@ -883,9 +890,10 @@ def write_to_stdin(self, line):
def create_new_client(self, give_focus=True, path=''):
"""Create a new client"""
self.master_clients += 1
name = "%d/A" % self.master_clients
client_id = dict(int_id=to_text_string(self.master_clients),
str_id='A')
cf = self._new_connection_file()
client = ClientWidget(self, name=name,
client = ClientWidget(self, id_=client_id,
history_filename=get_conf_path('history.py'),
config_options=self.config_options(),
additional_options=self.additional_options(),
Expand Down Expand Up @@ -1177,11 +1185,6 @@ def get_client_index_from_id(self, client_id):
if id(client) == client_id:
return index

def rename_client_tab(self, client):
"""Rename client's tab"""
index = self.get_client_index_from_id(id(client))
self.tabwidget.setTabText(index, client.get_name())

def get_related_clients(self, client):
"""
Get all other clients that are connected to the same kernel as `client`
Expand Down Expand Up @@ -1330,6 +1333,30 @@ def move_tab(self, index_from, index_to):
self.clients.insert(index_to, client)
self.sig_update_plugin_title.emit()

def rename_client_tab(self, client):
"""Rename client's tab"""
index = self.get_client_index_from_id(id(client))
self.tabwidget.setTabText(index, client.get_name())

def rename_tabs_after_change(self, given_name):
client = self.get_current_client()

# Rename current client tab to add str_id
if client.allow_rename:
client.given_name = given_name
self.rename_client_tab(client)

# Rename related clients
if client.allow_rename:
for cl in self.get_related_clients(client):
cl.given_name = given_name
self.rename_client_tab(cl)

def tab_name_editor(self):
"""Trigger the tab name editor."""
index = self.tabwidget.currentIndex()
self.tabwidget.tabBar().tab_name_editor.edit_tab(index)

#------ Public API (for help) ---------------------------------------------
def go_to_error(self, text):
"""Go to error if relevant"""
Expand Down Expand Up @@ -1419,35 +1446,41 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
"<b>%s</b>") % connection_file)
return

# Getting the master name that corresponds to the client
# Getting the master id that corresponds to the client
# (i.e. the i in i/A)
master_name = None
master_id = None
given_name = None
external_kernel = False
slave_ord = ord('A') - 1
kernel_manager = None

for cl in self.get_clients():
if connection_file in cl.connection_file:
if cl.get_kernel() is not None:
kernel_manager = cl.get_kernel()
connection_file = cl.connection_file
if master_name is None:
master_name = cl.name.split('/')[0]
new_slave_ord = ord(cl.name.split('/')[1])
if master_id is None:
master_id = cl.id_['int_id']
given_name = cl.given_name
new_slave_ord = ord(cl.id_['str_id'])
if new_slave_ord > slave_ord:
slave_ord = new_slave_ord

# If we couldn't find a client with the same connection file,
# it means this is a new master client
if master_name is None:
if master_id is None:
self.master_clients += 1
master_name = to_text_string(self.master_clients)
master_id = to_text_string(self.master_clients)
external_kernel = True

# Set full client name
name = master_name + '/' + chr(slave_ord + 1)
client_id = dict(int_id=master_id,
str_id=chr(slave_ord + 1))

# Creating the client
client = ClientWidget(self, name=name,
client = ClientWidget(self,
id_=client_id,
given_name=given_name,
history_filename=get_conf_path('history.py'),
config_options=self.config_options(),
additional_options=self.additional_options(),
Expand Down
4 changes: 2 additions & 2 deletions spyder/plugins/tests/test_ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ def test_load_kernel_file_from_id(ipyconsole, qtbot):
qtbot.wait(1000)

new_client = ipyconsole.get_clients()[1]
assert new_client.name == '1/B'
assert new_client.id_ == dict(int_id='1', str_id='B')


@flaky(max_runs=3)
Expand Down Expand Up @@ -533,7 +533,7 @@ def test_load_kernel_file(ipyconsole, qtbot):
with qtbot.waitSignal(new_shell.executed):
new_shell.execute('a = 10')

assert new_client.name == '1/B'
assert new_client.id_ == dict(int_id='1', str_id='B')
assert shell.get_value('a') == new_shell.get_value('a')


Expand Down
25 changes: 20 additions & 5 deletions spyder/widgets/ipythonconsole/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,26 +90,29 @@ class ClientWidget(QWidget, SaveHistoryMixin):

append_to_history = Signal(str, str)

def __init__(self, plugin, name, history_filename, config_options,
def __init__(self, plugin, id_,
history_filename, config_options,
additional_options, interpreter_versions,
connection_file=None, hostname=None,
menu_actions=None, slave=False,
external_kernel=False):
external_kernel=False, given_name=None):
super(ClientWidget, self).__init__(plugin)
SaveHistoryMixin.__init__(self, history_filename)

# --- Init attrs
self.name = name
self.id_ = id_
self.connection_file = connection_file
self.hostname = hostname
self.menu_actions = menu_actions
self.slave = slave
self.given_name = given_name

# --- Other attrs
self.options_button = None
self.stop_button = None
self.stop_icon = ima.icon('stop')
self.history = []
self.allow_rename = True

# --- Widgets
self.shellwidget = ShellWidget(config=config_options,
Expand Down Expand Up @@ -249,8 +252,18 @@ def show_kernel_error(self, error):

def get_name(self):
"""Return client name"""
return ((_("Console") if self.hostname is None else self.hostname)
+ " " + self.name)
if self.given_name is None:
# Name according to host
if self.hostname is None:
name = _("Console")
else:
name = self.hostname
# Adding id to name
client_id = self.id_['int_id'] + u'/' + self.id_['str_id']
name = name + u' ' + client_id
else:
name = self.given_name + u'/' + self.id_['str_id']
return name

def get_control(self):
"""Return the text widget (or similar) to give focus to"""
Expand Down Expand Up @@ -419,6 +432,8 @@ def kernel_restarted_message(self, msg):
stderr = self._read_stderr()
except:
stderr = None
except FileNotFoundError:
stderr = None

if stderr:
self.show_kernel_error('<tt>%s</tt>' % stderr)
Expand Down
127 changes: 121 additions & 6 deletions spyder/widgets/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
import sys

# Third party imports
from qtpy.QtCore import QByteArray, QMimeData, QPoint, Qt, Signal
from qtpy import PYQT5
from qtpy.QtCore import QByteArray, QMimeData, QPoint, Qt, Signal, QEvent
from qtpy.QtGui import QDrag
from qtpy.QtWidgets import (QApplication, QHBoxLayout, QMenu, QTabBar,
QTabWidget, QWidget)
QTabWidget, QWidget, QLineEdit)

# Local imports
from spyder.config.base import _
Expand All @@ -31,11 +32,103 @@
create_toolbutton)


class EditTabNamePopup(QLineEdit):
"""Popup on top of the tab to edit its name."""

def __init__(self, parent):
"""Popup on top of the tab to edit its name."""

# Variables
# Parent (main)
self.main = parent if parent is not None else self.parent()
# Track with tab is being edited
self.tab_index = None

# Widget setup
QLineEdit.__init__(self, parent=None)

# Slot to handle tab name update
self.editingFinished.connect(self.edit_finished)

# Even filter to catch clicks and ESC key
self.installEventFilter(self)

# Clean borders and no shadow to blend with tab
if PYQT5:
self.setWindowFlags(
Qt.Popup |
Qt.FramelessWindowHint |
Qt.NoDropShadowWindowHint
)
else:
self.setWindowFlags(
Qt.Popup |
Qt.FramelessWindowHint
)
self.setFrame(False)

# Align with tab name
self.setTextMargins(9, 0, 0, 0)

def eventFilter(self, widget, event):
"""Catch clicks outside the object and ESC key press."""
if ((event.type() == QEvent.MouseButtonPress and
not self.geometry().contains(event.globalPos())) or
(event.type() == QEvent.KeyPress and
event.key() == Qt.Key_Escape)):
# Exits editing
self.hide()
self.setFocus(False)
return True

# Event is not interessant, raise to parent
return QLineEdit.eventFilter(self, widget, event)

def edit_tab(self, index):
"""Activate the edit tab."""

# Sets focus, shows cursor
self.setFocus(True)

# Updates tab index
self.tab_index = index

# Gets tab size and shrinks to avoid overlapping tab borders
rect = self.main.tabRect(index)
rect.adjust(1, 1, -2, -1)

# Sets size
self.setFixedSize(rect.size())

# Places on top of the tab
self.move(self.main.mapToGlobal(rect.topLeft()))

# Copies tab name and selects all
self.setText(self.main.tabText(index))
self.selectAll()

if not self.isVisible():
# Makes editor visible
self.show()

def edit_finished(self):
"""On clean exit, update tab name."""
# Hides editor
self.hide()

if isinstance(self.tab_index, int) and self.tab_index >= 0:
# We are editing a valid tab, update name
tab_text = to_text_string(self.text())
self.main.setTabText(self.tab_index, tab_text)
self.main.sig_change_name.emit(tab_text)


class TabBar(QTabBar):
"""Tabs base class with drag and drop support"""
sig_move_tab = Signal((int, int), (str, int, int))
sig_change_name = Signal(str)

def __init__(self, parent, ancestor):
def __init__(self, parent, ancestor, rename_tabs=False):
QTabBar.__init__(self, parent)
self.ancestor = ancestor

Expand All @@ -48,6 +141,14 @@ def __init__(self, parent, ancestor):
self.setAcceptDrops(True)
self.setUsesScrollButtons(True)

# Tab name editor
self.rename_tabs = rename_tabs
if self.rename_tabs:
# Creates tab name editor
self.tab_name_editor = EditTabNamePopup(self)
else:
self.tab_name_editor = None

def mousePressEvent(self, event):
"""Reimplement Qt method"""
if event.button() == Qt.LeftButton:
Expand Down Expand Up @@ -111,7 +212,20 @@ def dropEvent(self, event):
self.sig_move_tab.emit(index_from, index_to)
event.acceptProposedAction()
QTabBar.dropEvent(self, event)


def mouseDoubleClickEvent(self, event):
"""Override Qt method to trigger the tab name editor."""
if self.rename_tabs is True and \
event.buttons() == Qt.MouseButtons(Qt.LeftButton):
# Tab index
index = self.tabAt(event.pos())
if index >= 0:
# Tab is valid, call tab name editor
self.tab_name_editor.edit_tab(index)
else:
# Event is not interesting, raise to parent
QTabBar.mouseDoubleClickEvent(self, event)


class BaseTabs(QTabWidget):
"""TabWidget with context menu and corner widgets"""
Expand Down Expand Up @@ -284,10 +398,11 @@ class Tabs(BaseTabs):
sig_move_tab = Signal(str, str, int, int)

def __init__(self, parent, actions=None, menu=None,
corner_widgets=None, menu_use_tooltips=False):
corner_widgets=None, menu_use_tooltips=False,
rename_tabs=False):
BaseTabs.__init__(self, parent, actions, menu,
corner_widgets, menu_use_tooltips)
tab_bar = TabBar(self, parent)
tab_bar = TabBar(self, parent, rename_tabs=rename_tabs)
tab_bar.sig_move_tab.connect(self.move_tab)
tab_bar.sig_move_tab[(str, int, int)].connect(
self.move_tab_from_another_tabwidget)
Expand Down

0 comments on commit fe8d342

Please sign in to comment.