Trying to produce monospaced output in pyqt browser - python

I have modified a short piece of pyqt code to produce real-time rendering of a user's expression. I have used sympy's pretty-printing function for this, however the output does not appear correctly as the QTextBrowser uses a proportional rather than a monospaced font.
As a beginner I would also welcome any other thoughts you had on the code.
Many thanks and best wishes,
Geddes
from __future__ import division
import sys
import sympy
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Form(QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
self.browser = QTextBrowser()
self.lineedit = QLineEdit("please type an expression")
self.lineedit.selectAll()
layout = QVBoxLayout()
layout.addWidget(self.browser)
layout.addWidget(self.lineedit)
self.setLayout(layout)
self.lineedit.setFocus()
self.connect(self.lineedit, SIGNAL("textChanged (const QString&)"),self.updateUi)
def updateUi(self):
text = unicode(self.lineedit.text())
for z in range(0,9):
text = text.replace('x'+str(z),'x^'+str(z))
text = text.replace(')'+str(z),')^'+str(z))
text = text.replace(str(z)+'x',str(z)+'*x')
text = text.replace(str(z)+'(',str(z)+'*(')
try:
self.browser.append(sympy.printing.pretty(sympy.sympify(text)))
self.browser.clear()
self.browser.append(sympy.printing.pretty(sympy.sympify(text)))
except:
if text=='': self.browser.clear()
app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()

You should be able to change the font with setFontFamily.
Concerning your code: I haven't really worked with PyQt yet (only some hacks like the font family in qbzr...), so I can't tell you if everything is okay. But the following is not a good idea:
except:
if text=='': self.browser.clear()
Never catch all exceptions with except:. This will also catch BaseExceptions like SystemExit, which shouldn't be caught unless you have a reason to do so. Always catch specific exceptions, or if you're at the highest level (before the unhandled exception handler is executed) and want to log errors, rather use except Exception: which will only handle exceptions based on Exception.
if text=='' - I think if not text is more "pythonic".

QTextBrowser inherits QTextEdit, so you can use the setCurrentFont(QFont) method to set a monospace font.
self.browser = QTextBrowser()
self.browser.setCurrentFont(QFont("Courier New")) #Or whatever monospace font family you want...
As for general comments on style, there's probably a way do change your text replacement stuff in updateUi() to regex, but I can't be sure without seeing sample data to figure out what you're trying to do.
Also, you should probably refactor
try:
self.browser.append(sympy.printing.pretty(sympy.sympify(text)))
self.browser.clear()
self.browser.append(sympy.printing.pretty(sympy.sympify(text)))
except:
if text=='': self.browser.clear()
Into something more like:
self.browser.clear()
try:
self.browser.append(sympy.printing.pretty(sympy.sympify(text)))
except:
if text=='': self.browser.clear()
Except probably catching the actual Exception you're expecting.
EDIT
Here's something for the equation normalizing it looks like you're trying to do, it works with lowercase a-z and real numbers:
def updateUi(self):
text = unicode(self.lineedit.text())
text = re.sub(r'(\d+)([\(]|[a-z])',r'\1*\2',text) #for multiplication
text = re.sub(r'([a-z]|[\)])(\d+)',r'\1^\2',text) #for exponentiation
The first pattern looks for 1 or more digits \d+ followed by an open parenthesis, or a single letter a-z [\(]|[a-z]. It uses parentheses to capture the digit part of the pattern and the variable part of the pattern, and inserts a * between them. \1*\2.
The second pattern looks for a variable a-z or a close parenthesis [a-z]|[\)], followed by one or more digits \d+. It uses the grouping parentheses to capture the digit and the variable again, and inserts a ^ between them \1^\2.
It's not quite perfect (doesn't handle xy --> x*y) but its closer. If you want to make a full computer algebra system you'll probably need to build a dedicated parser :)

Related

Validatecommand is called when I edit the entry programmatically

I am trying to create an entry with a very special validation. For that, I'm playing dirty with validatecommand. However, I got a problem:
When something in the entry is deleted, I can't tell if it was done with the delete or backspace key (and tutorial pages like this: https://www.pythontutorial.net/tkinter/tkinter-validation/ no substitution is indicated provide that information).
So, I decided to add a bind. The function that I link returns "break" and must take care of removing a character and inserting a space in its place.
The problem, as the title says, is that validatecommand even validates entry edits made with the insert and delete methods.
To avoid this I considered disabling validation (which always returns True) while I make the corresponding edits. But this could cause other entries not to be validated.
Is there a way to skip that validation when programmatically editing an entry?
I leave you this code so that you have some basis to help me:
from functools import partial
class ChrFormat:
def __init__(self):
self.charvalidators = []
def register_in(self, widget):
widget.config(validate="key", validatecommand=(widget.register(partial(self, widget)), "%d", "%i", "%P", "%s", "%S"))
def add(self, obj):
self.charvalidators.append(obj)
def __call__(self, widget, accion, index, new_text, old_text, char):
accion = int(accion)
index = int(index)
if(len(char) == 1):
if(accion == 1):
if(index < self.width):
for validator in self.charvalidators[index:]:
if(not isinstance(validator, str)):
break
index += 1
else:
return False
if(validator(char)):
widget.delete(index)
widget.insert(index, char)
widget.icursor(index + 1)
return (accion != 1)
def apply(self):
self.width = len(self.charvalidators)
self.null = "".join((part if(isinstance(part, str)) else " ") for part in self.charvalidators)
fecha = ChrFormat()
fecha.add(str.isdecimal)
fecha.add(str.isdecimal)
fecha.add("-")
fecha.add(str.isdecimal)
fecha.add(str.isdecimal)
fecha.add("-")
fecha.add(str.isdecimal)
fecha.add(str.isdecimal)
fecha.add(str.isdecimal)
fecha.add(str.isdecimal)
fecha.apply()
from tkinter import ttk
import tkinter as tk
root = tk.Tk()
sv = tk.StringVar()
entrada = ttk.Entry(textvariable=sv)
entrada.pack()
fecha.register_in(entrada)
sv.set(fecha.null)
I think I didn't explain myself well, sorry. What I'm looking for is that when the user presses backspace, it deletes a number and puts a space in its place. And something similar with delete. But I need to know which side of the cursor to put that space on.
Obviously, natural validation is the right thing for this, maybe do the validation through binds.
For those who know a bit about Clipper programming languaje, I want to mimic what happens when a picture is placed, such as '#r 999.999'. I would post a video, but I'm not in a good time to record and I didn't find any videos to prove it.
Is there a way to skip that validation when programmatically editing an entry?
The simplest solution is to set the validate option to "none" before making the edits. You then can turn the validation back on via after_idle as documented in the official tk documentation
widget.configure(validate="none")
widget.delete(index)
widget.insert(index, char)
widget.icursor(index + 1)
widget.after_idle(lambda: widget.configure(validate="key"))

How to change default cursor mode from insert to overwrite when entering QLineEdit

As the title implies my goal is to change default cursor mode (insert character) to overwrite mode.
I have a validator attached to QLineEdit object
expression = QtCore.QRegExp(r'(^[A-Z0-9_]{4})-([A-Z0-9_]{6})-([A-Z0-9_]{6})-([A-Z0-9_]{4})-([A-Z0-9_]{2})-([A-Z0-9_]{6})')
object = QtGui.QRegExpValidator(expression, self)
and default value of QLineEdit component is:
object.setText('____-______-______-____-__-______')
When entering the QLineObject and in order to write anything I have to select a underscore, change it for desired character, select another one and so on. Alternatively I can select entire string, delete it and then write complete string. The point is however that sometimes I have to change some string on different position and leave unchanged underscores intact. Selecting and changing character after character is workable but tedious. Changing cursor mode from insert to overwrite when cursor focus is on one object would be much cleaner solution.
QLineEdit does not have an overwrite mode. But QTextEdit and QPlainTextEdit have overwriteMode and setOverwriteMode(bool). If you can use a QTextEdit, you could subclass it and have your own keyReleaseEvent. Here's a quick example to test overwrite mode. Every time the user presses the Insert key on the keyboard, overwriteMode is toggled.
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit)
from PyQt5 import QtCore
import sys
class OverwriteTextBox(QTextEdit):
def __init__(self, parent=None):
super(OverwriteTextBox, self).__init__(parent=parent)
def keyReleaseEvent(self, event):
key = event.key()
if (key == QtCore.Qt.Key_Insert):
print('insert')
self.setOverwriteMode(not self.overwriteMode())
else:
# release keyrelease for normal behaviors
super().keyReleaseEvent(event)
class Example(QMainWindow):
def __init__(self, parent=None):
super(Example, self).__init__(parent=parent)
self.setGeometry(300, 200, 570, 450)
self.setWindowTitle("Overwrite test")
self.textbox = OverwriteTextBox(parent=self)
self.textbox.setGeometry(50, 50, 200, 50)
app = QApplication(sys.argv)
example = Example()
example.show()
sys.exit(app.exec_() )
Seems like you need to set the inputMask() property.
In your case, it should be:
object.setInputMask(
'>NNNN-NNNNNN-NNNNNN-NNNN-NN-NNNNNN;_'
The N mask character is for alphanumeric characters, when preceded by > it means all following letters will be uppercase. Any character set after the semicolon is used as a placeholder.
Just for reference. The solution proposed by musicamante seems to work very well. There is only one modification. Provided the regex expression used by QRegExpValidator is the following:
expression = QtCore.QRegExp(r'(^[A-Z0-9_]{4})-([A-Z0-9_]{6})-([A-Z0-9_]{6})-([A-Z0-9_]{4})-([A-Z0-9_]{2})-([A-Z0-9_]{6})')
The input mask should use space as the character used for empty characters
object.setInputMask('>XXXX-XXXXXX-XXXXXX-XXXX-XX-XXXXXX; ')
With underscore as one of character in regex string and replacement of empties in InputMask I have experienced some strange results like this
instead of expected
Anyway once again thank for quick help.

Text edit always uppercase

I'm writing a screenwriting application with PySide. What I want is to turn the characters to uppercase while the user is typing.
The following piece of code gives a runtime error saying "maximum recursion depth exceeded" every time I add a character. I get what it means and why it's happening but is there a different way?
self.cursor = self.recipient.textCursor()
self.cursor.movePosition(QTextCursor.StartOfLine)
self.cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor)
self.curtext = self.cursor.selectedText()
if len(self.curtext) > len(self.prevText):
self.cursor.insertText(self.curtext.upper())
self.cursor.clearSelection()
self.prevText = self.curtext
The code above runs whenever the text in the text edit widget is changed. The if statment prevents the code from running when the user hasn't insert text.
You get a recursion error probably because when fixing the input to uppercase you are changing your content and again triggering the very same fixing routine. Also you constantly change the whole line while only a part has changed and needs to be fixed.
Fortunately Qt can do this itself using a QTextCharFormat. Here is the example that automatically keeps all text in a QLineEdit upper case. And you can do much more with it like underlining or making text bold...
Example:
from PySide import QtGui
app = QtGui.QApplication([])
widget = QtGui.QTextEdit()
fmt = QtGui.QTextCharFormat()
fmt.setFontCapitalization(QtGui.QFont.AllUppercase)
widget.setCurrentCharFormat(fmt)
widget.show()
app.exec_()

Tkinter international bind

Is there a way in Tkinter to bind a combination of keys that will work in all keyboard layouts? (bind by scancode)
For example, I need 'Control-Z' binding that will work with the same physical key in the lower left corner of the keyboard in all layouts, such as:
* Russian layout,
* Greek layout, etc.
Here's what I tried:
from Tkinter import *
root=Tk()
def f(event):
print 'undo'
button1=Button(root, text=u'Button')
button1.pack()
button1.bind('<Control-z>', f)
root.mainloop()
It doesn't work for Russian and Greek keyboard layouts.
Update-2:
I did some more experiments with Windows and now the general rule is like that:
1) If the language is based on latin character set, keys are mapped "by value" (German, French, Dvorak) so that the same action is mapped to different physical keys.
2) If it is not (eg Russian, Greek), then all major accelerators are mapped "by position" (to match the corresponding English letter usually marked on the same key).
Only the second case needs special attention. Any ideas if this is implemented in some lib already?
Update-3
It is simply reproduced even without Russian keyboard or Russian Windows.
1) Start->Control Panel->Regional and Language Options
2) Language->Details
3) Add Russian language.
That's it. Now Alt-Shift will switch you to Russian and you'll be able to type the following funny symbols:
another Alt-Shift will switch you back.
Forget what Wikipedia says about phonetic Russian layouts. They are not used these days. At least inside Russia.
All Windows applications (including wxPython ones) use Ctrl-я for undo, Ctrl-ч for cut, Ctrl-с for copy and so on.
Thanks to #acw1668 for help!
You need to do something like this to use hotkeys with any language layout (the callback from this example is running when Control key is pressed, and prints the key that is pressed in the same time with Control:
from tkinter import *
def callback(event):
if (event.state & 4 > 0):
print("Ctrl-%s pressed" % chr(event.keycode))
root = Tk()
root.bind("<Key>", callback)
root.mainloop()
PS: This example was checked on Windows 10 when English, Ukrainian, Russian, Arabic, Amharic, Armenian, Greek, Georgian, French, Chinese, Japanese and other language layouts were used.
What I'm primarily interested in is Russian layout in Windows.
The quick and dirty workaround I currently use is:
import Tkinter
def copy(event):
print 'Ctrl-C'
root = Tkinter.Tk()
root.bind('<Control-ntilde>', copy)
root.mainloop()
which could potentially lead to a conflict with <Ctrl + actual ntilde> in some other language.
It could be overcome if I could determine which layout is currently active, thus second question: Tkinter determine keyboard layout.
Another drawback is that due to 'universal' treatment of modifier keys it also fires when I press Ctrl-Alt-V, but that's another story as it applies to English layout as well.
I have a partial and rather ugly solution for this. In the code below I have a window with Text widget, which have some "in-box" connection between standard Ctrl+C keyboard events and their proper handling. However, if I simply change the keyboard layout to, say, Russian, these functions do not work anymore. To solve the problem I re-wrote implementation for these events, and now everything works fine. But I feel slightly frustrated about such a solution. Does anyone have any better ideas?.. For instance, is there a way to trigger (or mimic) "normal" key press in python tkinter?
import tkinter
root = tkinter.Tk()
class MyWind (tkinter.Frame):
def __init__(self, parent):
tkinter.Frame.__init__(self, parent)
self.create_UI()
def create_UI(self):
text_field = tkinter.Text(self)
text_field.insert(tkinter.END, "Hello world")
text_field.pack()
def print_event(event):
print ("Event character code <char>: '%s'" % event.char)
print (" Event key symbol <keysym>: '%s'" % event.keysym)
print (" Event key code <keycode>: '%s'" % event.keycode)
def work_out_event(event): # Here is the solution
widget_type = type(event.widget)
if widget_type == tkinter.Text:
content = event.widget.selection_get()
print ("Selected content = '%s'" % content)
root.clipboard_clear()
root.clipboard_append(content)
def lurker1(event):
print ("Crtl + C (english) pressed!")
print_event(event)
def lurker2(event):
print ("Crtl + C (russian) pressed!")
print_event(event)
work_out_event(event)
root.bind("<Control-c>", lurker1) # "C" character with the english keyboard layout
root.bind("<Control-ntilde>", lurker2) # "C" character with the russian keyboard layout
root.app = MyWind(root)
root.app.pack()
root.mainloop()
Another option already suggested in the old 1999 is to switch from Tkinter to wxPython where accelerators handling is done for all types of keyboard layouts automatically (eg Editor example here: http://wiki.wxpython.org/AnotherTutorial).
def copy(event):
print 'Ctrl-C'
master.clipboard_append('text')
and it works!

Formatting a spinbutton's display in PyGObject/GTK+3

I'm in the process of porting an application from PyGTK to PyGObject. Mostly it's going well because mostly I did conventional things with PyGTK. But there's one somewhat ugly hack I was using to display the value of a SpinButton as currency (with a $ in front of it).
I originally got this solution from the PyGTK mailing list back in the days before Stack Overflow. As you can see, the magic happens on the input and output signals:
import gtk, ctypes
def _currency_input(spinbutton, gpointer):
text = spinbutton.get_text()
if text.startswith('$'):
text = text[1:]
double = ctypes.c_double.from_address(hash(gpointer))
double.value = float(text)
return True
def _currency_output(spinbutton):
text = '$%.*f' % (int(spinbutton.props.digits),
spinbutton.props.adjustment.value)
spinbutton.set_text(text)
return True
def format_spinbutton_currency(spinbutton):
spinbutton.connect('input', _currency_input)
spinbutton.connect('output', _currency_output)
def _test():
s = gtk.SpinButton(gtk.Adjustment(value=1, lower=0, upper=1000,
step_incr=1))
s.props.digits = 2
format_spinbutton_currency(s)
w = gtk.Window()
w.props.border_width = 12
w.add(s)
w.show_all()
w.connect('destroy', gtk.main_quit)
gtk.main()
if __name__ == '__main__':
_test()
Doing my best to translate that into PyGObject, I came up with:
from gi.repository import Gtk
import ctypes
def _currency_input(spinbutton, gpointer):
text = spinbutton.get_text()
if text.startswith('$'):
text = text[1:]
double = ctypes.c_double.from_address(hash(gpointer))
double.value = float(text)
return True
def _currency_output(spinbutton):
text = '$%.*f' % (int(spinbutton.props.digits),
spinbutton.get_value())
spinbutton.set_text(text)
return True
def format_spinbutton_currency(spinbutton):
spinbutton.connect('input', _currency_input)
spinbutton.connect('output', _currency_output)
def _test():
s = Gtk.SpinButton()
s.set_adjustment(Gtk.Adjustment(value=1, lower=0, upper=1000,
step_increment=1))
s.props.digits = 2
format_spinbutton_currency(s)
w = Gtk.Window()
w.props.border_width = 12
w.add(s)
w.show_all()
w.connect('destroy', Gtk.main_quit)
Gtk.main()
if __name__ == '__main__':
_test()
Unfortunately, this doesn't work. It shows up fine initially, but when I click the up or down error, it crashes and I see:
/usr/lib/python2.7/dist-packages/gi/types.py:43: Warning: g_value_get_double: assertion `G_VALUE_HOLDS_DOUBLE (value)' failed
return info.invoke(*args, **kwargs)
Segmentation fault
Any idea what this error message means?
Or what part of my code might not work under PyGObject?
Or, better yet, how to fix this error?
Or, even better still, a more straightforward solution to my original problem (displaying a $ in front of the SpinButton contents)?
From the PyGtk documentation:
http://developer.gnome.org/pygtk/stable/class-gtkspinbutton.html#signal-gtkspinbutton--input
The "input" signal is emitted when the value changes. The value_ptr is a GPointer to the value that cannot be accessed from PyGTK. This signal cannot be handled in PyGTK.
So, I'm atonished to see something like:
double = ctypes.c_double.from_address(hash(gpointer))
This is a real hack, so you got and awful error "Segmentation Fault" which means your are messing in memory you don't have to, and it's quite generic, it happens for example when in C you try to manually access a memory pointer not handled by your application.
This will be a hard one, I tried for one hour and all approaches I tried had problems. I know is not and answer, but as a workaround and if you only need the currency symbol (not grouping) you can experiment adding a currency symbol image to the inherited Gtk.Entry set_icon_from_pixbuf():
(Obviously set the image to a currency image)
Kind regards
Or, even better still, a more straightforward solution to my original problem (displaying a $ in front of the SpinButton contents)?
I got this working by hooking up the GTKSpinButton's output signal to a simple handler:
def onSpinOutput(self, spinbutton):
adjustment = spinbutton.get_adjustment()
spinbutton.set_text(str(int(adjustment.get_value())) + "%")
return True
In my case I'm adding a percentage sign after the number, but you can easily change this to insert something else in front instead. Note that this sets the text and not the value of the entry; this will not work for numerical spin buttons.

Categories

Resources