I am writing an application in python with urwid.
I need an Edit widget with autocompletion. Haven't seen one in the documentation so I have tried implementing it on my own based on the pop_up example.
However, I am faced with the fact that the pop_up widget has the focus which is a problem because:
The cursor in the edit widget is not visible. When using the left and right arrow keys without counting how often you have pressed them you do not know where the next character will be inserted.
All user input goes to the pop_up widget and not to the PopUpLauncher, although most of the key events are meant for the edit widget. I cannot call Edit.keypress because I don't know the size of the edit widget. Therefore I need to duplicate code from urwid.
How can I set the PopUpLauncher to have the focus?
From there on this answer might be helpful.
A strongly simplified version of my custom widget class:
#!/usr/bin/env python
import urwid
class AutoCompleteEdit(urwid.PopUpLauncher):
CMD_INSERT_SELECTED = "complete"
command_map = urwid.CommandMap()
command_map['tab'] = CMD_INSERT_SELECTED
def __init__(self, get_completions):
self.edit_widget = urwid.Edit()
self.__super.__init__(self.edit_widget)
self.get_completions = get_completions
# ------- user input ------
def keypress(self, size, key):
cmd = self.command_map[key]
if cmd is None:
out = self.__super.keypress(size, key)
self.update_completions()
return out
return self.__super.keypress(size, key)
def forwarded_keypress(self, key):
if self.edit_widget.valid_char(key):
#if (isinstance(key, text_type) and not isinstance(self._caption, text_type)):
# # screen is sending us unicode input, must be using utf-8
# # encoding because that's all we support, so convert it
# # to bytes to match our caption's type
# key = key.encode('utf-8')
self.edit_widget.insert_text(key)
self.update_completions()
return
cmd = self.command_map[key]
if cmd == self.CMD_INSERT_SELECTED:
self.insert_selected()
return
elif cmd == urwid.CURSOR_LEFT:
p = self.edit_widget.edit_pos
if p == 0:
return key
p = urwid.move_prev_char(self.edit_widget.edit_text, 0, p)
self.edit_widget.set_edit_pos(p)
elif cmd == urwid.CURSOR_RIGHT:
p = self.edit_widget.edit_pos
if p >= len(self.edit_widget.edit_text):
return key
p = urwid.move_next_char(self.edit_widget.edit_text, p, len(self.edit_widget.edit_text))
self.edit_widget.set_edit_pos(p)
elif key == "backspace":
self.edit_widget.pref_col_maxcol = None, None
if not self.edit_widget._delete_highlighted():
p = self.edit_widget.edit_pos
if p == 0:
return key
p = urwid.move_prev_char(self.edit_widget.edit_text,0,p)
self.edit_widget.set_edit_text(self.edit_widget.edit_text[:p] + self.edit_widget.edit_text[self.edit_widget.edit_pos:])
self.edit_widget.set_edit_pos(p)
elif key == "delete":
self.edit_widget.pref_col_maxcol = None, None
if not self.edit_widget._delete_highlighted():
p = self.edit_widget.edit_pos
if p >= len(self.edit_widget.edit_text):
return key
p = urwid.move_next_char(self.edit_widget.edit_text,p,len(self.edit_widget.edit_text))
self.edit_widget.set_edit_text(self.edit_widget.edit_text[:self.edit_widget.edit_pos] + self.edit_widget.edit_text[p:])
else:
return key
self.update_completions()
return key
def update_completions(self):
i = self.edit_widget.edit_pos
text = self.edit_widget.edit_text[:i]
prefix, completions = self.get_completions(text)
self.prefix = prefix
self.completions = completions
if not self.completions:
if self.is_open():
self.close_pop_up()
return
if not self.is_open():
self.open_pop_up()
self._pop_up_widget.update_completions(completions)
def insert_selected(self):
text = self._pop_up_widget.get_selected()
i = self.edit_widget.edit_pos - len(self.prefix)
assert i >= 0
text = text[i:]
self.edit_widget.insert_text(text)
self.close_pop_up()
# ------- internal ------
def is_open(self):
return self._pop_up_widget
# ------- implementation of abstract methods ------
def create_pop_up(self):
return PopUpList(self.forwarded_keypress)
def get_pop_up_parameters(self):
height = len(self.completions)
width = max(len(x) for x in self.completions)
return {'left':len(self.prefix), 'top':1, 'overlay_width':width, 'overlay_height':height}
class PopUpList(urwid.WidgetWrap):
ATTR = 'popup-button'
ATTR_FOCUS = 'popup-button-focus'
def __init__(self, keypress_callback):
self.body = urwid.SimpleListWalker([urwid.Text("")])
widget = urwid.ListBox(self.body)
widget = urwid.AttrMap(widget, self.ATTR)
self.__super.__init__(widget)
self.keypress_callback = keypress_callback
def update_completions(self, completions):
self.body.clear()
for x in completions:
widget = ListEntry(x)
widget = urwid.AttrMap(widget, self.ATTR, self.ATTR_FOCUS)
self.body.append(widget)
def get_selected(self):
focus_widget, focus_pos = self.body.get_focus()
return self.body[focus_pos].original_widget.text
def keypress(self, size, key):
key = self.keypress_callback(key)
if key:
return super().keypress(size, key)
class ListEntry(urwid.Text):
#https://stackoverflow.com/a/56759094
_selectable = True
signals = ["click"]
def keypress(self, size, key):
"""
Send 'click' signal on 'activate' command.
"""
if self._command_map[key] != urwid.ACTIVATE:
return key
self._emit('click')
def mouse_event(self, size, event, button, x, y, focus):
"""
Send 'click' signal on button 1 press.
"""
if button != 1 or not urwid.util.is_mouse_press(event):
return False
self._emit('click')
return True
if __name__ == '__main__':
palette = [
('popup-button', 'white', 'dark blue'),
('popup-button-focus', 'white,standout', 'dark blue'),
('error', 'dark red', 'default'),
]
completions = ["hello", "hi", "world", "earth", "universe"]
def get_completions(start):
i = start.rfind(" ")
if i == -1:
prefix = ""
else:
i += 1
prefix = start[:i]
start = start[i:]
return prefix, [word for word in completions if word.startswith(start)]
widget = AutoCompleteEdit(get_completions)
widget = urwid.Filler(widget)
#WARNING: note the pop_ups=True
urwid.MainLoop(widget, palette, pop_ups=True).run()
I have found a solution for the first part of the problem at least (visibility of the cursor): overriding the render method in the AutoCompleteEdit class:
def render(self, size, focus=False):
if self.is_open():
focus = True
return self.__super.render(size, focus)
The second problem, code duplication, still remains.
Actually it's more than just code duplication because some functionality (moving the cursor to the very beginning or the very end) is using the size argument of the keypress method. I don't know the size so I can't just copy it out of there.
So if someone knows a better way I would be grateful.
Related
I have a python problem that creates a box and puts items in the box and with the help of the list command I can see what's in my box. One example of the execution may go as:
next command> newbox bedroom-box-05
next command> add bedroom-box-05 pillow 3
next command> list bedroom-box-05
Box "bedroom-box-05" contains 3 items.
3 pillow
There are some issues with class MovingBox() as I cannot change the main functions.
class MovingBox():
"""A class for keeping track of the contents ofa moving box. """
def __init__(self,*args):
self = dict()
def add_item(self,key,value =[]):
setattr(self,key,value)
self.add_item=value
def list_content(self,key,value =[]):
setattr(self, key, value)
self.list_content = value
key1 = Keys() # parens
key1.list_content(value)
DON'T CHANGE ANYTHING AFTER THIS LINE
def convert_str_to_int(word):
"""
Converts the parameter string *word* in to an integer value.
"""
try:
result = int(word)
except ValueError:
return None
return result
def newbox(all_boxes, list_of_additional_info):
if len(list_of_additional_info) != 1:
print("Error: wrong number of initial data: can't create a new box.")
return
box_name = list_of_additional_info[0]
all_boxes[box_name] = MovingBox(box_name)
def add_to_box(all_boxes, list_of_additional_info):
if len(list_of_additional_info) != 3:
print("Error: wrong number of elements: can't add into a box.")
return
box_name, item_name, item_count = list_of_additional_info
item_count = convert_str_to_int(item_count)
if item_count is None:
print("Error: not a number: can't add to a box.")
return
if box_name not in all_boxes:
print("Error: box does not exist: can't add to a box.")
return
all_boxes[box_name].add_item(item_name, item_count)
def list_box_content(all_boxes, list_of_additional_info):
"""Displays the contents of a single box in *all_boxes* """
if len(list_of_additional_info) != 1:
print("Error: wrong number of elements: can't list contents.")
return
box_name = list_of_additional_info[0]
if box_name not in all_boxes:
print("Error: box does not exist: can't list content.")
return
all_boxes[box_name].list_content()
def main():
boxes = {}
while True:
command_line = input("next command> ").strip()
if command_line == "":
break
command_words = command_line.split()
first_word = command_words[0]
list_of_other_words = command_words[1:]
if first_word == "quit":
break
elif first_word == "newbox":
newbox(boxes, list_of_other_words)
elif first_word == "add":
add_to_box(boxes, list_of_other_words)
elif first_word == "list":
list_box_content(boxes, list_of_other_words)
if __name__ == "__main__":
main()
Based on the way this class is being used in the code that you're not supposed to change, I don't think you've understood what the MovingBox is supposed to contain/model. Here are examples of how MovingBox is being used:
MovingBox(box_name)
# creates a box named box_name
all_boxes[box_name].add_item(item_name, item_count)
# increments the count of item_name by item_count
all_boxes[box_name].list_content()
# lists the content of the box
The immediate error you're hitting is that list_content isn't supposed to take a parameter, but the larger problem is that what you've tried to implement doesn't match up at all with what the rest of the code is trying to do -- e.g. your implementation of add_item seems to be trying to add a unique item with an arbitrary value (that defaults to [], which has its own problems), when the calling code is actually providing a count of a type of item.
Since MovingBox is seemingly supposed to just be a counter with a custom interface, I'd implement it as a wrapper around collections.Counter:
from collections import Counter
class MovingBox():
"""A class for keeping track of the contents of a moving box. """
def __init__(self, name: str):
self.name = name
self.contents = Counter()
def add_item(self, name: str, count: int) -> None:
self.contents[name] += count
def list_content(self) -> None:
total = sum(self.contents.values())
print(f'Box "{self.name}" contains {total} items.')
for name, count in self.contents.items():
print(f'{count} {name}')
>>> box = MovingBox("a box")
>>> box.add_item("pillow", 3)
>>> box.list_content()
Box "a box" contains 3 items.
3 pillow
I am trying to make a Chess game, and I am making the graphics using the tkinter library in Python. I made a subclass of Canvas called chessPiece. Using the following code, I distributed it onto the tkinter grid:
for i in range(8):
for j in range(8):
if (i%2==0 and j%2==0) or (i%2==1 and j%2==1):
color='blanched almond'
else:
color='olive drab'
self.cells.append(chessPiece(self,color,(i,j)))
self.cells[i*8+j].grid(row=i,column=j)
and it works perfectly. The real problem comes later, when I try to add 5 buttons, 4 to a new 8th row, and 1 to a new 9th row, using the following code:
self.master.qButton = Button(text='Queen',command=self.createQueen)
self.master.rButton = Button(text='Rook',command=self.createRook)
self.master.bButton = Button(text='Bishop',command=self.createBishop)
self.master.kButton = Button(text='Knight',command=self.createKnight)
self.master.conButton = Button(text='CONFIRM PIECE',command=self.conPiece)
self.master.qButton.grid(row=8,column=0,columnspan=2)
self.master.rButton.grid(row=8,column=2,columnspan=2)
self.master.bButton.grid(row=8,column=4,columnspan=2)
self.master.kButton.grid(row=8,column=6,columnspan=2)
self.master.conButton.grid(row=9,column=2,columnspan=4)
Instead of fitting nicely underneath the board, the first button is centered under the board and takes up the entire space, and the rest of the four buttons are off to the side of the board, making some 9th column, while I am only trying to make a second column!
I am posting the entire code here. Note that I have only put in partial logic for pawns, and that I have made it so that all the black pawns are white for now. The code I am having problems with is the promote(self) function in the chessPiece class, which is when a pawn is being promoted, so just take a piece with a white pawn to promote it quickly and see the outcome of my code.
Full Code:
from tkinter import *
from PIL import ImageTk, Image
def cL(coord):
return coord[0]*8+coord[1]
class chessPiece(Canvas):
pics = {\
'bpawn':'bPawn.png',\
'bbish':'bBishop.png',\
'bking':'bKing.png',\
'brook':'bRook.png',\
'bknit':'bKnight.png',\
'bquen':'bQueen.png',\
'wpawn':'wPawn.png',\
'wbish':'wBishop.png',\
'wking':'wKing.png',\
'wrook':'wRook.png',\
'wknit':'wKnight.png',\
'wquen':'wQueen.png',\
}
def __init__(self,master,BG,coord):
self.bgColor = BG
self.coord=coord
self.cLoc=coord[0]*8+coord[1]
self.width=50
self.height=50
Canvas.__init__(self,master,width=self.width,height=self.height,bg=BG,\
highlightthickness=0,relief=RAISED)
self.piece = 'none'
self.bind("<Button-1>",self.move)
def createPiece(self,pieceName, size=(50,50)):
self.createImage(pieceName,size)
self.piece = pieceName
def createImage(self,pieceName,size):
filename = self.pics[pieceName]
self.im = Image.open(filename)
self.resizePic(size)
self.img=ImageTk.PhotoImage(self.im)
self.pic = self.create_image(self.width/2,self.height/2,anchor=CENTER,\
image=self.img)
def resizePic(self,size):
self.im = self.im.resize(size,Image.ANTIALIAS)
def removePiece(self):
self.piece = 'none'
self.delete(self.pic)
def move(self, misc = ''):
if self['bg']=='lightgreen':
self.unhighlight()
self.master.lastCell=-1
return 0
if not self.master.hasHighlighted:
self.master.cells[self.master.lastCell].unhighlight()
self.highlight()
self.master.lastCell=self.cLoc
else:
if self.master.lastCell==-1:
return 0
if self.master.validMove(self.master.cells[self.master.lastCell], self):
self.master.cells[self.master.lastCell].unhighlight()
self.highlight()
self.createPiece(self.master.cells[self.master.lastCell].piece)
self.master.cells[self.master.lastCell].removePiece()
if (self.piece=='bpawn' and self.coord[0]==7) or (self.piece=='wpawn' and self.coord[0]==0):
self.promote()
self.master.lastCell=self.cLoc
self.master.toggleTurn()
def highlight(self):
self['bg']='lightgreen'
self.master.hasHighlighted = True
def unhighlight(self):
self['bg']=self.bgColor
self.master.hasHighlighted = False
def promote(self):
self.master.qButton = Button(text='Queen',command=self.createQueen)
self.master.rButton = Button(text='Rook',command=self.createRook)
self.master.bButton = Button(text='Bishop',command=self.createBishop)
self.master.kButton = Button(text='Knight',command=self.createKnight)
self.master.conButton = Button(text='CONFIRM PIECE',command=self.conPiece)
self.master.qButton.grid(row=8,column=0,columnspan=2)
self.master.rButton.grid(row=8,column=2,columnspan=2)
self.master.bButton.grid(row=8,column=4,columnspan=2)
self.master.kButton.grid(row=8,column=6,columnspan=2)
self.master.conButton.grid(row=9,column=2,columnspan=4)
def createQueen(self):
if self.piece[0]=='w':
self.createPiece('wquen')
else:
self.createPiece('bquen')
def createRook(self):
if self.piece[0]=='w':
self.createPiece('wrook')
else:
self.createPiece('brook')
def createBishop(self):
if self.piece[0]=='w':
self.createPiece('wbish')
else:
self.createPiece('bbish')
def createKnight(self):
if self.piece[0]=='w':
self.createPiece('wknit')
else:
self.createPiece('bknit')
def conPiece(self):
if self.piece == 'bpawn' or self.piece=='wpawn':
return 0
self.master.qButton.grid_remove()
self.master.rButton.grid_remove()
self.master.bButton.grid_remove()
self.master.kButton.grid_remove()
self.master.conButton.grid_remove()
class chessBoard(Frame):
def __init__(self,master):
Frame.__init__(self,master)
self.grid()
self.cells=[]
for i in range(8):
for j in range(8):
if (i%2==0 and j%2==0) or (i%2==1 and j%2==1):
color='blanched almond'
else:
color='olive drab'
self.cells.append(chessPiece(self,color,(i,j)))
self.cells[i*8+j].grid(row=i,column=j)
self.rowconfigure(8,minsize=8)
self.cells[0].createPiece('brook')
self.cells[1].createPiece('bknit')
self.cells[2].createPiece('bbish')
self.cells[3].createPiece('bquen')
self.cells[4].createPiece('bking')
self.cells[5].createPiece('bbish')
self.cells[6].createPiece('bknit')
self.cells[7].createPiece('brook')
for i in range(8):
self.cells[cL((1,i))].createPiece('wpawn')
self.cells[cL((6,i))].createPiece('wpawn')
self.cells[cL((7,0))].createPiece('wrook')
self.cells[cL((7,1))].createPiece('wknit')
self.cells[cL((7,2))].createPiece('wbish')
self.cells[cL((7,3))].createPiece('wquen')
self.cells[cL((7,4))].createPiece('wking')
self.cells[cL((7,5))].createPiece('wbish')
self.cells[cL((7,6))].createPiece('wknit')
self.cells[cL((7,7))].createPiece('wrook')
self.lastCell = -1
self.turn = 0
self.hasHighlighted = False
def toggleTurn(self):
self.turn = (self.turn+1)%2
def validMove(self, oCell, nCell):
if oCell.piece=='wpawn':
return self.validMoveWP(oCell, nCell)
elif oCell.piece=='bpawn':
return self.validMoveBP(oCell, nCell)
else:
return False
def validMoveWP(self, oCell, nCell):
if self.turn != 0:
return False
if oCell.coord[1]==nCell.coord[1]:
if oCell.coord[0]==6:
if self.cells[cL((5,oCell.coord[1]))].piece != 'none':
return False
if (nCell.coord[0]==5 or nCell.coord[0]==4):
return True
elif (nCell.coord[1]==oCell.coord[1]):
return True
elif abs(oCell.coord[1]-nCell.coord[1])==1 and \
oCell.coord[0]-nCell.coord[0]==1 and \
nCell.piece != 'none':
return True
return False
def validMoveBP(self, oCell, nCell):
if self.turn != 1:
return False
if oCell.coord[1]==nCell.coord[1]:
if oCell.coord[0]==1:
if self.cells[cL((2,oCell.coord[1]))].piece != 'none':
return False
if (nCell.coord[0]==2 or nCell.coord[0]==3):
return True
elif (nCell.coord[1]==oCell.coord[1]):
return True
elif abs(oCell.coord[1]-nCell.coord[1])==1 and \
oCell.coord[0]-nCell.coord[0]==-1 and \
nCell.piece != 'none':
return True
return False
def play_chess():
root = Tk()
root.title('Chess')
game = chessBoard(root)
root.mainloop()
play_chess()
You will need to save the pictures named as the picture-descriptions I have given them. For example, you will need to save the black pawn picture as bPawn.png.
Thanks in advance!
In my QtreeViewI use a 'QStandardItemModel` for displaying several items with individual properties. I want to avoid that the item will not be mixed. e.g. Bananas should be moveable to vegetables (same child level) but not below Asia (higher level), moving to Asia - Fruits is ok (same child level)
Sample
I've worked with .itemChanged but it appears to late. I need a signal before it will be dropped and the item where it will be dropped. I tried eventFilterand get
event.type() == QtCore.QEvent.DragMove:
but how do I get the index of the item where the item will be dropped to decide it its in the same child level?
To solve this problem I have created a custom mimetype that sends the information of the index and the level of depth that it has, and it will only move those indexes that have the same level as the children of the destination.
class TreeView(QTreeView):
customMimeType = "application/x-customqstandarditemmodeldatalist"
def __init__(self, *args, **kwargs):
QTreeView.__init__(self, *args, **kwargs)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setDragEnabled(True)
self.viewport().setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QTreeView.InternalMove)
def itemsToPixmap(self, indexes):
rect = self.viewport().visibleRegion().boundingRect()
pixmap = QPixmap(rect.size())
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
for index in indexes:
painter.drawPixmap(self.visualRect(index), self.viewport().grab(self.visualRect(index)))
return pixmap
def mimeTypes(self):
mimetypes = QTreeView.mimeTypes(self)
mimetypes.append(TreeView.customMimeType)
return mimetypes
def startDrag(self, supportedActions):
drag = QDrag(self)
mimedata = self.model().mimeData(self.selectedIndexes())
encoded = QByteArray()
stream = QDataStream(encoded, QIODevice.WriteOnly)
self.encodeData(self.selectedIndexes(), stream)
mimedata.setData(TreeView.customMimeType, encoded)
drag.setMimeData(mimedata)
px = self.itemsToPixmap(self.selectedIndexes())
drag.setPixmap(px)
drag.setHotSpot(self.viewport().mapFromGlobal(QCursor.pos()) - QPoint(self.horizontalOffset(),
self.verticalOffset()))
drag.exec_(supportedActions)
def encodeData(self, items, stream):
stream.writeInt32(len(items))
for item in items:
p = item
rows = []
while p.isValid():
rows.append(p.row())
p = p.parent()
stream.writeInt32(len(rows))
for row in reversed(rows):
stream.writeInt32(row)
def dropEvent(self, event):
if event.source() == self:
if event.mimeData().hasFormat(TreeView.customMimeType):
encoded = event.mimeData().data(TreeView.customMimeType)
items = self.decodeData(encoded, event.source())
ix = self.indexAt(event.pos())
current = self.model().itemFromIndex(ix)
p = current
level = 1
while p:
p = p.parent()
level += 1
for item, ilevel in items:
if level == ilevel:
item.parent().takeRow(item.row())
current.appendRow(item)
self.clearSelection()
event.acceptProposedAction()
else:
event.ignore()
def decodeData(self, encoded, tree):
items = []
rows = []
stream = QDataStream(encoded, QIODevice.ReadOnly)
while not stream.atEnd():
nItems = stream.readInt32()
for i in range(nItems):
path = stream.readInt32()
row = []
for j in range(path):
row.append(stream.readInt32())
rows.append(row)
for row in rows:
it = self.model().item(row[0])
for r in row[1:]:
it = it.child(r)
items.append((it, len(row)))
return items
A complete example can be found in the following link
The following changes allow to move an item inside a row of childs, but not outside. Still working for changing the cursor if target is not the same level.
def dropEvent(self, event):
if event.source() == self:
if event.mimeData().hasFormat(TreeView.customMimeType):
encoded = event.mimeData().data(TreeView.customMimeType)
items = self.decodeData(encoded, event.source())
ix = self.indexAt(event.pos())
current = self.model().itemFromIndex(ix)
p = current
level = 0
while p:
p = p.parent()
level += 1
for item, ilevel in items:
if level == ilevel:
item.parent().takeRow(item.row())
current.parent().insertRow(current.row(),item)
self.clearSelection()
event.acceptProposedAction()
else:
event.ignore()
I want to investigate how to make a small user interface in which a user can type some letters and gets some suggestions based on a given data source (list here) which makes searches easier. For this purpose i am using Qt's QCompleter class.
In the matching elements the typed letters shall be highlighted with HTML like the example in the code below: Au<b>st</b>ria.
Finally i merged some SO answers (see How to make item view render rich (html) text in Qt) and tutorials to a small standalone module:
from PySide import QtCore, QtGui
class HTMLDelegate(QtGui.QStyledItemDelegate):
""" From: https://stackoverflow.com/a/5443112/1504082 """
def paint(self, painter, option, index):
options = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(options, index)
if options.widget is None:
style = QtGui.QApplication.style()
else:
style = options.widget.style()
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(option.rect.width())
options.text = ""
style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# Highlighting text if item is selected
# if options.state & QtGui.QStyle.State_Selected:
# ctx.palette.setColor(QtGui.QPalette.Text,
# options.palette.color(QtGui.QPalette.Active,
# QtGui.QPalette.HighlightedText))
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText,
options)
painter.save()
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
options = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(options, index)
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(options.rect.width())
return QtCore.QSize(doc.size().width(), doc.size().height())
class CustomQCompleter(QtGui.QCompleter):
""" Implement "contains" filter mode as the filter mode "contains" is not
available in Qt < 5.2
From: https://stackoverflow.com/a/7767999/1504082 """
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
self.delegate = HTMLDelegate()
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
# see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
class InnerProxyModel(QtGui.QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
# model index mapping by row, 1d model => column is always 0
index = self.sourceModel().index(sourceRow, 0, sourceParent)
source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
# performs case insensitive matching
# return True if item shall stay in th returned filtered data
# return False to reject an item
return local_completion_prefix.lower() in source_data.lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
# #todo: Why to be set here again?
self.popup().setItemDelegate(self.delegate)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
class AutoCompleteEdit(QtGui.QLineEdit):
""" Basically from:
http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
"""
def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
super(AutoCompleteEdit, self).__init__()
# settings
self._separator = separator
self._addSpaceAfterCompleting = addSpaceAfterCompleting
# completer
self._completer = CustomQCompleter(self)
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.model = QtGui.QStringListModel(list_data)
self._completer.setModel(self.model)
# connect the completer to the line edit
self._completer.setWidget(self)
# trigger insertion of the selected completion when its activated
self.connect(self._completer,
QtCore.SIGNAL('activated(QString)'),
self._insertCompletion)
self._ignored_keys = [QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape,
QtCore.Qt.Key_Tab]
def _insertCompletion(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
It will remove the already typed string with the one of the completion.
"""
stripped_text = self.text()[:-len(self._completer.completionPrefix())]
extra_text = completion # [-extra:]
if self._addSpaceAfterCompleting:
extra_text += ' '
self.setText(stripped_text + extra_text)
def textUnderCursor(self):
text = self.text()
textUnderCursor = ''
i = self.cursorPosition() - 1
while i >= 0 and text[i] != self._separator:
textUnderCursor = text[i] + textUnderCursor
i -= 1
return textUnderCursor
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._ignored_keys:
event.ignore()
return
super(AutoCompleteEdit, self).keyPressEvent(event)
completionPrefix = self.textUnderCursor()
if completionPrefix != self._completer.completionPrefix():
self._updateCompleterPopupItems(completionPrefix)
if len(event.text()) > 0 and len(completionPrefix) > 0:
self._completer.complete()
if len(completionPrefix) == 0:
self._completer.popup().hide()
def _updateCompleterPopupItems(self, completionPrefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(completionPrefix)
# self._completer.popup().setCurrentIndex(
# self._completer.completionModel().index(0, 0))
if __name__ == '__main__':
def demo():
import sys
app = QtGui.QApplication(sys.argv)
values = ['Germany',
'Au<b>st</b>ria',
'Switzerland',
'Hungary',
'The United Kingdom of Great Britain and Northern Ireland']
editor = AutoCompleteEdit(values)
window = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
hbox.addWidget(editor)
window.setLayout(hbox)
window.show()
sys.exit(app.exec_())
demo()
My problem is the suggestion of user Timo in the answer https://stackoverflow.com/a/5443112/1504082:
After line: 'doc.setHtml(options.text)', you need to set also doc.setTextWidth(option.rect.width()), otherwise the delegate wont render longer content correctly in respect to target drawing area. For example does not wrap words in QListView.
So i did this to avoid cropping of long text in the completer's popup. But i get the following output:
Where does this additional vertical margin come from?
I investigated this a bit and i see that the sizeHint method of HTMLDelegate is sometimes called with an options parameter which contains a rectangle with attributes (0, 0, 0, 0). And the display behaviour finally changes after the call of doc.setTextWidth(options.rect.width()). But i couldnt finally find out who calls it with this parameter and how i could properly fix this.
Can somebody explain where this comes from and how i can fix this porperly?
Finally i found another way to realize it using the idea of https://stackoverflow.com/a/8036666/1504082. Its much more forward for me without all this custom drawing things which i dont understand yet :)
from PySide import QtCore, QtGui
class TaskDelegate(QtGui.QItemDelegate):
# based on https://stackoverflow.com/a/8036666/1504082
# https://doc.qt.io/archives/qt-4.7/qitemdelegate.html#drawDisplay
# https://doc.qt.io/archives/qt-4.7/qwidget.html#render
margin_x = 5
margin_y = 3
def drawDisplay(self, painter, option, rect, text):
label = self.make_label(option, text)
# calculate render anchor point
point = rect.topLeft()
point.setX(point.x() + self.margin_x)
point.setY(point.y() + self.margin_y)
label.render(painter, point, renderFlags=QtGui.QWidget.DrawChildren)
def sizeHint(self, option, index):
# get text using model and index
text = index.model().data(index)
label = self.make_label(option, text)
return QtCore.QSize(label.width(), label.height() + self.margin_y)
def make_label(self, option, text):
label = QtGui.QLabel(text)
if option.state & QtGui.QStyle.State_Selected:
p = option.palette
p.setColor(QtGui.QPalette.WindowText,
p.color(QtGui.QPalette.Active,
QtGui.QPalette.HighlightedText)
)
label.setPalette(p)
label.setStyleSheet("border: 1px dotted black")
# adjust width according to widget's target width
label.setMinimumWidth(self.target_width - (2 * self.margin_x))
label.setMaximumWidth(self.target_width - self.margin_x)
label.setWordWrap(True)
label.adjustSize()
return label
class CustomQCompleter(QtGui.QCompleter):
""" Implement "contains" filter mode as the filter mode "contains" is not
available in Qt < 5.2
From: https://stackoverflow.com/a/7767999/1504082 """
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
self.delegate = TaskDelegate()
# widget not set yet
# self.delegate.target_width = self.widget().width()
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
# see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
class InnerProxyModel(QtGui.QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
# model index mapping by row, 1d model => column is always 0
index = self.sourceModel().index(sourceRow, 0, sourceParent)
source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
# performs case insensitive matching
# return True if item shall stay in th returned filtered data
# return False to reject an item
return local_completion_prefix.lower() in source_data.lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
# #todo: Why to be set here again?
# -> rescale popup list items to widget width
self.delegate.target_width = self.widget().width()
self.popup().setItemDelegate(self.delegate)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
class AutoCompleteEdit(QtGui.QLineEdit):
""" Basically from:
http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
"""
def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
super(AutoCompleteEdit, self).__init__()
# settings
self._separator = separator
self._addSpaceAfterCompleting = addSpaceAfterCompleting
# completer
self._completer = CustomQCompleter(self)
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.model = QtGui.QStringListModel(list_data)
self._completer.setModel(self.model)
# connect the completer to the line edit
self._completer.setWidget(self)
# trigger insertion of the selected completion when its activated
self.connect(self._completer,
QtCore.SIGNAL('activated(QString)'),
self._insertCompletion)
self._ignored_keys = [QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape,
QtCore.Qt.Key_Tab]
def _insertCompletion(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
It will remove the already typed string with the one of the completion.
"""
stripped_text = self.text()[:-len(self._completer.completionPrefix())]
extra_text = completion # [-extra:]
if self._addSpaceAfterCompleting:
extra_text += ' '
self.setText(stripped_text + extra_text)
def textUnderCursor(self):
text = self.text()
textUnderCursor = ''
i = self.cursorPosition() - 1
while i >= 0 and text[i] != self._separator:
textUnderCursor = text[i] + textUnderCursor
i -= 1
return textUnderCursor
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._ignored_keys:
event.ignore()
return
super(AutoCompleteEdit, self).keyPressEvent(event)
completionPrefix = self.textUnderCursor()
if completionPrefix != self._completer.completionPrefix():
self._updateCompleterPopupItems(completionPrefix)
if len(event.text()) > 0 and len(completionPrefix) > 0:
self._completer.complete()
if len(completionPrefix) == 0:
self._completer.popup().hide()
def _updateCompleterPopupItems(self, completionPrefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(completionPrefix)
# self._completer.popup().setCurrentIndex(
# self._completer.completionModel().index(0, 0))
if __name__ == '__main__':
def demo():
import sys
app = QtGui.QApplication(sys.argv)
values = ['Germany',
'Au<b>st</b>ria',
'Switzerland',
'Hungary',
'The United Kingdom of Great Britain and Northern Ireland',
'USA']
editor = AutoCompleteEdit(values)
window = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
hbox.addWidget(editor)
window.setLayout(hbox)
window.show()
sys.exit(app.exec_())
demo()
I have got a PyQT widget interpreter working, the code picked up from here is as follows:
import os
import re
import sys
import code
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MyInterpreter(QWidget):
def __init__(self, parent):
super(MyInterpreter, self).__init__(parent)
hBox = QHBoxLayout()
self.setLayout(hBox)
self.textEdit = PyInterp(self)
# this is how you pass in locals to the interpreter
self.textEdit.initInterpreter(locals())
self.resize(650, 300)
self.centerOnScreen()
hBox.addWidget(self.textEdit)
hBox.setMargin(0)
hBox.setSpacing(0)
def centerOnScreen(self):
# center the widget on the screen
resolution = QDesktopWidget().screenGeometry()
self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
(resolution.height() / 2) - (self.frameSize().height() / 2))
class PyInterp(QTextEdit):
class InteractiveInterpreter(code.InteractiveInterpreter):
def __init__(self, locals):
code.InteractiveInterpreter.__init__(self, locals)
def runIt(self, command):
code.InteractiveInterpreter.runsource(self, command)
def __init__(self, parent):
super(PyInterp, self).__init__(parent)
sys.stdout = self
sys.stderr = self
self.refreshMarker = False # to change back to >>> from ...
self.multiLine = False # code spans more than one line
self.command = '' # command to be ran
self.printBanner() # print sys info
self.marker() # make the >>> or ... marker
self.history = [] # list of commands entered
self.historyIndex = -1
self.interpreterLocals = {}
# setting the color for bg and text
palette = QPalette()
palette.setColor(QPalette.Base, QColor(0, 0, 0))
palette.setColor(QPalette.Text, QColor(0, 255, 0))
self.setPalette(palette)
self.setFont(QFont('Courier', 12))
# initilize interpreter with self locals
self.initInterpreter(locals())
def printBanner(self):
self.write(sys.version)
self.write(' on ' + sys.platform + '\n')
self.write('PyQt4 ' + PYQT_VERSION_STR + '\n')
msg = 'Type !hist for a history view and !hist(n) history index recall'
self.write(msg + '\n')
def marker(self):
if self.multiLine:
self.insertPlainText('... ')
else:
self.insertPlainText('>>> ')
def initInterpreter(self, interpreterLocals=None):
if interpreterLocals:
# when we pass in locals, we don't want it to be named "self"
# so we rename it with the name of the class that did the passing
# and reinsert the locals back into the interpreter dictionary
selfName = interpreterLocals['self'].__class__.__name__
interpreterLocalVars = interpreterLocals.pop('self')
self.interpreterLocals[selfName] = interpreterLocalVars
else:
self.interpreterLocals = interpreterLocals
self.interpreter = self.InteractiveInterpreter(self.interpreterLocals)
def updateInterpreterLocals(self, newLocals):
className = newLocals.__class__.__name__
self.interpreterLocals[className] = newLocals
def write(self, line):
self.insertPlainText(line)
self.ensureCursorVisible()
def clearCurrentBlock(self):
# block being current row
length = len(self.document().lastBlock().text()[4:])
if length == 0:
return None
else:
# should have a better way of doing this but I can't find it
[self.textCursor().deletePreviousChar() for x in xrange(length)]
return True
def recallHistory(self):
# used when using the arrow keys to scroll through history
self.clearCurrentBlock()
if self.historyIndex <> -1:
self.insertPlainText(self.history[self.historyIndex])
return True
def customCommands(self, command):
if command == '!hist': # display history
self.append('') # move down one line
# vars that are in the command are prefixed with ____CC and deleted
# once the command is done so they don't show up in dir()
backup = self.interpreterLocals.copy()
history = self.history[:]
history.reverse()
for i, x in enumerate(history):
iSize = len(str(i))
delta = len(str(len(history))) - iSize
line = line = ' ' * delta + '%i: %s' % (i, x) + '\n'
self.write(line)
self.updateInterpreterLocals(backup)
self.marker()
return True
if re.match('!hist\(\d+\)', command): # recall command from history
backup = self.interpreterLocals.copy()
history = self.history[:]
history.reverse()
index = int(command[6:-1])
self.clearCurrentBlock()
command = history[index]
if command[-1] == ':':
self.multiLine = True
self.write(command)
self.updateInterpreterLocals(backup)
return True
return False
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
# proper exit
self.interpreter.runIt('exit()')
if event.key() == Qt.Key_Down:
if self.historyIndex == len(self.history):
self.historyIndex -= 1
try:
if self.historyIndex > -1:
self.historyIndex -= 1
self.recallHistory()
else:
self.clearCurrentBlock()
except:
pass
return None
if event.key() == Qt.Key_Up:
try:
if len(self.history) - 1 > self.historyIndex:
self.historyIndex += 1
self.recallHistory()
else:
self.historyIndex = len(self.history)
except:
pass
return None
if event.key() == Qt.Key_Home:
# set cursor to position 4 in current block. 4 because that's where
# the marker stops
blockLength = len(self.document().lastBlock().text()[4:])
lineLength = len(self.document().toPlainText())
position = lineLength - blockLength
textCursor = self.textCursor()
textCursor.setPosition(position)
self.setTextCursor(textCursor)
return None
if event.key() in [Qt.Key_Left, Qt.Key_Backspace]:
# don't allow deletion of marker
if self.textCursor().positionInBlock() == 4:
return None
if event.key() in [Qt.Key_Return, Qt.Key_Enter]:
# set cursor to end of line to avoid line splitting
textCursor = self.textCursor()
position = len(self.document().toPlainText())
textCursor.setPosition(position)
self.setTextCursor(textCursor)
line = str(self.document().lastBlock().text())[4:] # remove marker
line.rstrip()
self.historyIndex = -1
if self.customCommands(line):
return None
else:
try:
line[-1]
self.haveLine = True
if line[-1] == ':':
self.multiLine = True
self.history.insert(0, line)
except:
self.haveLine = False
if self.haveLine and self.multiLine: # multi line command
self.command += line + '\n' # + command and line
self.append('') # move down one line
self.marker() # handle marker style
return None
if self.haveLine and not self.multiLine: # one line command
self.command = line # line is the command
self.append('') # move down one line
self.interpreter.runIt(self.command)
self.command = '' # clear command
self.marker() # handle marker style
return None
if self.multiLine and not self.haveLine: # multi line done
self.append('') # move down one line
self.interpreter.runIt(self.command)
self.command = '' # clear command
self.multiLine = False # back to single line
self.marker() # handle marker style
return None
if not self.haveLine and not self.multiLine: # just enter
self.append('')
self.marker()
return None
return None
# allow all other key events
super(PyInterp, self).keyPressEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyInterpreter(None)
win.show()
sys.exit(app.exec_())
Is there an easy way of getting some tab completion going just for local symbols ?
I think you are referring to rlcompleter's Completer object.
You can used it like so:
from rlcompleter import Completer
line = str(...)
completer = Completer(self.interpreter.locals)
suggestion = completer.complete(line, 0)
self.insertPlainText(suggestion)
The numeric argument indicates the n-th suggestion, and you can iterate over it until it returns None.
For example, say we have
>>> my_data = '012345'
then
>>> completer.complete('my_', 0)
'my_data'
>>> completer.complete('my_data.s', 0)
'my_data.split('
>>> completer.complete('my_data.s', 1)
'my_data.splitlines('
Note that while the code above uses interpreter.locals, you can apply a wider search (but be sure to provide a dictionary).
If you want to save yourself some time - take a look at spyderlib, it contains a widget that embeds an interactive Python interpreter with some interaction sugar such as code completion. The bits of specific interest are
spyderlib/widgets/sourcecode/codeeditor.py
spyderlib/shell.py
spyderlib/editor.py
spyderlib/widgets/externalshell/pythonshell.py
spyderlib/utiils/module_completion.py
spyderlib/plugins/externalconsole.py
spyderlib/plugins/console.py
spyderlib/plugins/editor.py
The only caveat I have with spyderlib is that you can't just use that doodad on it's own - somewhere I have a version I extracted that contains the bare minimum of support modules needed to run. If you run into the same problem I did regarding the bloat send me a msg and I'll check my stuff into github for you to grab.
I also seem to remember there's an Qt based interactive Python interpreter widget that is used in NumPy or SciPy - I think it originally came from the ipython project however. It's pretty nice because it actually splits the interpeter from the execution of code - so if your code crashes, your interpreter doesn't crash with it. But, in that case you can't modify the Pythonic contents of other threads.. The spyderlib version can work both ways.
I have an open-source PyQt based Python interpreter that you can find here: http://docs.projexsoftware.com/api/projexui/
The specific class is the XConsoleEdit found in projexui.widgets.xconsoleedit. It has auto-completion built-in.
Hope that helps!
I get auto complete from rlcompleter2, but there are two problems in the following code,
import xxx as yyy auto complete on yyy doesn't work
the locals() are not copied into the interactiveinterpreter, I tried to use this code in Autodesk Maya, eg, run x=3 in maya script editor, and then run x in the pyqt interpreter, it says NameError: name 'x' is not defined. if you do not use maya, this error can be reproduced from external python interpreter as well, first define some variable, then launch this ui, the variable is not copied into the interpreter in the ui.
import os
import re
import sys
import code
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MyInterpreter(QWidget):
def __init__(self, parent):
super(MyInterpreter, self).__init__(parent)
hBox = QHBoxLayout()
self.setLayout(hBox)
self.textEdit = PyInterp(self)
# this is how you pass in locals to the interpreter
self.textEdit.initInterpreter(locals())
self.resize(850, 400)
# self.centerOnScreen()
hBox.addWidget(self.textEdit)
hBox.setMargin(0)
hBox.setSpacing(0)
def centerOnScreen(self):
# center the widget on the screen
resolution = QDesktopWidget().screenGeometry()
self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
(resolution.height() / 2) - (self.frameSize().height() / 2))
class PyInterp(QTextEdit):
class InteractiveInterpreter(code.InteractiveInterpreter):
def __init__(self, locals):
code.InteractiveInterpreter.__init__(self, locals)
def runIt(self, command):
code.InteractiveInterpreter.runsource(self, command)
def __init__(self, parent):
super(PyInterp, self).__init__(parent)
sys.stdout = self
sys.stderr = self
self.refreshMarker = False # to change back to >>> from ...
self.multiLine = False # code spans more than one line
self.command = '' # command to be ran
self.printBanner() # print sys info
self.marker() # make the >>> or ... marker
self.history = [] # list of commands entered
self.historyIndex = -1
self.interpreterLocals = {}
# setting the color for bg and text
# palette = QPalette()
# palette.setColor(QPalette.Base, QColor(0, 0, 0))
# palette.setColor(QPalette.Text, QColor(0, 255, 0))
# self.setPalette(palette)
self.setFont(QFont('Courier', 10))
# initilize interpreter with self locals
self.initInterpreter(locals())
from rlcompleter2 import Completer
self.completer = Completer()
def printBanner(self):
self.write(sys.version)
self.write(' on ' + sys.platform + '\n')
self.write('PyQt4 ' + PYQT_VERSION_STR + '\n')
# msg = 'Type !hist for a history view and !hist(n) history index recall'
# self.write(msg + '\n')
def marker(self):
if self.multiLine:
self.insertPlainText('... ')
else:
self.insertPlainText('>>> ')
def initInterpreter(self, interpreterLocals=None):
if interpreterLocals:
# when we pass in locals, we don't want it to be named "self"
# so we rename it with the name of the class that did the passing
# and reinsert the locals back into the interpreter dictionary
selfName = interpreterLocals['self'].__class__.__name__
interpreterLocalVars = interpreterLocals.pop('self')
self.interpreterLocals[selfName] = interpreterLocalVars
else:
self.interpreterLocals = interpreterLocals
self.interpreter = self.InteractiveInterpreter(self.interpreterLocals)
def updateInterpreterLocals(self, newLocals):
className = newLocals.__class__.__name__
self.interpreterLocals[className] = newLocals
def write(self, line):
self.insertPlainText(line)
self.ensureCursorVisible()
def clearCurrentBlock(self):
# block being current row
length = len(self.document().lastBlock().text()[4:])
if length == 0:
return None
else:
# should have a better way of doing this but I can't find it
[self.textCursor().deletePreviousChar() for x in xrange(length)]
return True
def recallHistory(self):
# used when using the arrow keys to scroll through history
self.clearCurrentBlock()
if self.historyIndex <> -1:
self.insertPlainText(self.history[self.historyIndex])
return True
def customCommands(self, command):
if command == '!hist': # display history
self.append('') # move down one line
# vars that are in the command are prefixed with ____CC and deleted
# once the command is done so they don't show up in dir()
backup = self.interpreterLocals.copy()
history = self.history[:]
history.reverse()
for i, x in enumerate(history):
iSize = len(str(i))
delta = len(str(len(history))) - iSize
line = line = ' ' * delta + '%i: %s' % (i, x) + '\n'
self.write(line)
self.updateInterpreterLocals(backup)
self.marker()
return True
if re.match('!hist\(\d+\)', command): # recall command from history
backup = self.interpreterLocals.copy()
history = self.history[:]
history.reverse()
index = int(command[6:-1])
self.clearCurrentBlock()
command = history[index]
if command[-1] == ':':
self.multiLine = True
self.write(command)
self.updateInterpreterLocals(backup)
return True
return False
def keyPressEvent(self, event):
if event.key() == Qt.Key_Tab:
line = str(self.document().lastBlock().text())[4:]
self.completer.construct(line)
if len(self.completer.rl_matches) == 1:
self.clearCurrentBlock()
self.insertPlainText(self.completer.rl_matches[0])
else:
print 'repeat:', self.completer.repeated
mod = self.completer.repeated % len(self.completer.completions)
if mod == 0:
# print '\n'.join(self.completer.rl_matches)
col_print(self.completer.rl_matches)
else:
print ' '
print '\n'.join(self.completer.rl_matches)
# print self.completer.rl_matches
self.marker()
self.insertPlainText(line)
return
if event.key() == Qt.Key_Escape:
# proper exit
self.interpreter.runIt('exit()')
if event.key() == Qt.Key_Down:
if self.historyIndex == len(self.history):
self.historyIndex -= 1
try:
if self.historyIndex > -1:
self.historyIndex -= 1
self.recallHistory()
else:
self.clearCurrentBlock()
except:
pass
return None
if event.key() == Qt.Key_Up:
try:
if len(self.history) - 1 > self.historyIndex:
self.historyIndex += 1
self.recallHistory()
else:
self.historyIndex = len(self.history)
except:
pass
return None
if event.key() == Qt.Key_Home:
# set cursor to position 4 in current block. 4 because that's where
# the marker stops
blockLength = len(self.document().lastBlock().text()[4:])
lineLength = len(self.document().toPlainText())
position = lineLength - blockLength
textCursor = self.textCursor()
textCursor.setPosition(position)
self.setTextCursor(textCursor)
return None
if event.key() in [Qt.Key_Left, Qt.Key_Backspace]:
# don't allow deletion of marker
# if qt version < 4.7, have to use position() - block().position()
if self.textCursor().positionInBlock() == 4:
return None
if event.key() in [Qt.Key_Return, Qt.Key_Enter]:
# set cursor to end of line to avoid line splitting
textCursor = self.textCursor()
position = len(self.document().toPlainText())
textCursor.setPosition(position)
self.setTextCursor(textCursor)
line = str(self.document().lastBlock().text())[4:] # remove marker
line.rstrip()
self.historyIndex = -1
if self.customCommands(line):
return None
else:
try:
line[-1]
self.haveLine = True
if line[-1] == ':':
self.multiLine = True
self.history.insert(0, line)
except:
self.haveLine = False
if self.haveLine and self.multiLine: # multi line command
self.command += line + '\n' # + command and line
self.append('') # move down one line
self.marker() # handle marker style
return None
if self.haveLine and not self.multiLine: # one line command
self.command = line # line is the command
self.append('') # move down one line
self.interpreter.runIt(self.command)
self.command = '' # clear command
self.marker() # handle marker style
return None
if self.multiLine and not self.haveLine: # multi line done
self.append('') # move down one line
self.interpreter.runIt(self.command)
self.command = '' # clear command
self.multiLine = False # back to single line
self.marker() # handle marker style
return None
if not self.haveLine and not self.multiLine: # just enter
self.append('')
self.marker()
return None
return None
# allow all other key events
super(PyInterp, self).keyPressEvent(event)
# http://stackoverflow.com/a/30861871/2052889
def col_print(lines, term_width=90, indent=0, pad=2):
n_lines = len(lines)
if n_lines == 0:
return
col_width = max(len(line) for line in lines)
n_cols = int((term_width + pad - indent)/(col_width + pad))
n_cols = min(n_lines, max(1, n_cols))
col_len = int(n_lines/n_cols) + (0 if n_lines % n_cols == 0 else 1)
if (n_cols - 1) * col_len >= n_lines:
n_cols -= 1
cols = [lines[i*col_len: i*col_len + col_len] for i in range(n_cols)]
rows = list(zip(*cols))
rows_missed = zip(*[col[len(rows):] for col in cols[:-1]])
rows.extend(rows_missed)
for row in rows:
print(" "*indent + (" "*pad).join(line.ljust(col_width)
for line in row))
def main():
app = QApplication(sys.argv)
win = MyInterpreter(None)
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
desired effect:
https://gfycat.com/DistantScrawnyCivet
current effect:
https://gfycat.com/DeafeningHeavyBoto