def render(self, data, node_name): label_txt = data.get('label', None) font = Font(family="Helvetica", size=12) h = font.metrics("linespace") + 6 if label_txt: w = font.measure(label_txt) + 8 else: w = font.measure("....") + 2 self.config(width=w, height=h) marker_options = { 'fill': data.get('color', 'orange'), 'outline': 'orange' } if data.get('circle', False) or data.get('type', 'NOTLAN') == 'LAN': self.create_oval(0, 0, w - 1, h - 1, **marker_options) self.config(width=w, height=h) if label_txt: self.create_text((w) / 2, (h) / 2, text=label_txt, font=font, fill="black") else: self.create_rectangle(0, 0, w, h, **marker_options) if label_txt: self.create_text(w / 2, h / 2, text=label_txt, font=font, fill="black")
def render(self, data, node_name): # Figure out what size the text we want is. label_txt = data.get('label', None) font = Font(family="Helvetica", size=12) h = font.metrics("linespace") + 1 if label_txt: w = font.measure(label_txt) + 2 else: w = font.measure("....") + 2 self.config(width=w, height=h) marker_options = { 'fill': data.get('color', 'blue'), 'outline': 'white' } if data.get('circle', None): self.create_oval(0, 0, w, h, **marker_options) self.config(width=w, height=h) if label_txt: self.create_text(w / 2, h / 2, text=label_txt, font=font, fill="white") else: self.create_rectangle(0, 0, w, h, **marker_options) if label_txt: self.create_text(w / 2, h / 2, text=label_txt, font=font, fill="white")
def displayAutomata(self): for item in self.canvasitems: self.automataCanvas.delete(item) if self.selectedButton == 0: header = "e-NFA" automata = self.nfa if self.dotFound: image = self.nfaimg imagefile = self.nfaimagefile elif self.selectedButton == 1: header = "DFA" automata = self.dfa if self.dotFound: image = self.dfaimg imagefile = self.dfaimagefile elif self.selectedButton == 2: header = "Minimized DFA" automata = self.minDFA if self.dotFound: image = self.mindfaimg imagefile = self.mindfaimagefile font = Font(family="times", size=20) (w, h) = (font.measure(header), font.metrics("linespace")) headerheight = h + 10 itd = self.automataCanvas.create_text(10, 10, text=header, font=font, anchor=NW) self.canvasitems.append(itd) [text, linecount] = automata.getPrintText() font = Font(family="times", size=13) (w, h) = (font.measure(text), font.metrics("linespace")) textheight = headerheight + linecount * h + 20 itd = self.automataCanvas.create_text(10, headerheight + 10, text=text, font=font, anchor=NW) self.canvasitems.append(itd) if self.dotFound: itd = self.automataCanvas.create_image(10, textheight, image=image, anchor=NW) self.canvasitems.append(itd) totalwidth = imagefile.size[0] + 10 totalheight = imagefile.size[1] + textheight + 10 else: totalwidth = self.cwidth + 10 totalheight = textheight + 10 if totalheight < self.cheight: totalheight = self.cheight if totalwidth < self.cwidth: totalwidth = self.cwidth self.automataCanvas.config(scrollregion=(0, 0, totalwidth, totalheight))
def count_lines(self, s): """Count the number of lines in a given text. Before calculation, the tab width and line length of the text are fetched, so that up-to-date values are used. Lines are counted as if the string was wrapped so that lines are never over linewidth characters long. Tabs are considered tabwidth characters long. """ # Tab width is configurable tabwidth = self.editwin.get_tk_tabwidth() # Get the Text widget's size linewidth = self.editwin.text.winfo_width() # Deduct the border and padding linewidth -= 2*sum([int(self.editwin.text.cget(opt)) for opt in ('border', 'padx')]) # Get the Text widget's font font = Font(self.editwin.text, name=self.editwin.text.cget('font')) # Divide the size of the Text widget by the font's width. # According to Tk8.5 docs, the Text widget's width is set # according to the width of its font's '0' (zero) character, # so we will use this as an approximation. # see: http://www.tcl.tk/man/tcl8.5/TkCmd/text.htm#M-width linewidth //= font.measure('0') return count_lines_with_wrapping(s, linewidth, tabwidth)
def text_extents(self, style, text): """ The text extents are calculated using tkinter.Font """ with InternalWindow() as window: font_type = "" if style["bold"]: font_type += "bold" if style["italic"]: if len(font_type) > 0: font_type += " " font_type += "italic" # Create the new font object. font = Font(window, (style["font"], -int(style["size"]*FONT_SCALING), font_type)) # Query the data width = font.measure(text) metrics = font.metrics() return { "width": width / float(FONT_SCALING), "height": metrics["linespace"] / float(FONT_SCALING), "ascent": metrics["ascent"] / float(FONT_SCALING), "descent": metrics["descent"] / float(FONT_SCALING) }
def __init__( self, master: Union[tk.Frame, tk.Tk], x: int, y: int, horizontal_anchor: HorizontalAnchor, vertical_anchor: VerticalAnchor, txt: str, font: tkf.Font, txt_color: str, color: str, ): super().__init__( master=master, text=txt, font=font, padx=0, pady=0, fg=txt_color, bg=color, bd=0, ) txt_width = font.measure(txt) txt_height = font.metrics("linespace") xloc, yloc = self._get_position( horizontal_anchor, vertical_anchor, x, y, txt_width, txt_height ) self.place(x=xloc, y=yloc)
def create_widgets(self): ''' creates GUI for app ''' # expand widget to fill the grid # self.columnconfigure(1, weight=1, pad=100) # self.rowconfigure(1, weight=1, pad=20) self.textv = Text(self, relief=SUNKEN) self.textv.grid(row=1, column=1, columnspan=2, sticky=E + W) efont = Font(family="Monospace", size=14) self.textv.configure(font=efont) self.textv.config( wrap=NONE, # wrap = "word" undo=True, # Tk 8.4 width=60, tabs=(efont.measure(' ' * 4), )) self.textv.focus() self.scry = Scrollbar(self, orient=VERTICAL, command=self.textv.yview) self.scry.grid(row=1, column=3, sticky=E + N + S) # use N+S+E self.textv['yscrollcommand'] = self.scry.set self.scrx = Scrollbar(self, orient=HORIZONTAL, command=self.textv.xview) self.scrx.grid(row=2, column=1, columnspan=2, sticky=E + W + N) self.textv['xscrollcommand'] = self.scrx.set btnSave = Button(self, text='Open', command=self.on_btn_open_clicked) btnSave.grid(row=3, column=1) btnClose = Button(self, text='Save', command=self.on_btn_save_clicked) btnClose.grid(row=3, column=2)
def create_widgets(self): ''' creates GUI for app ''' # expand widget to fill the grid # self.columnconfigure(1, weight=1, pad=100) # self.rowconfigure(1, weight=1, pad=20) # customize widget style when using ttk... # style = Style() # style.configure("TButton", width=10) root.geometry("300x300") self.txt = Text(self) self.txt.grid(row=1, column=1) efont = Font(family="Ubuntu Mono", size=14) self.txt.configure(font=efont) self.txt.config(wrap = "word", # wrap = NONE undo = True, # Tk 8.4 width = 80, tabs = (efont.measure(' ' * 4),)) self.txt.focus() self.txt.insert("1.0", "right click to Copy & Paste ...") self.popup_menu = Menu(self, tearoff = 0) self.popup_menu.add_command(label = "Copy", command = lambda:self.clipbrd(1)) self.popup_menu.add_command(label = "Paste", command = lambda:self.clipbrd(2)) self.popup_menu.add_separator() self.popup_menu.add_command(label = "say bye", command = exit) self.txt.bind("<Button-3>",self.do_popup)
def run(): global x, first, work_integral, b w.configure(background='black') k = 70 f_text = 'Avenir Next Ultra Light' font = Font(family=f_text, size=k) text = 'The trig function is ' c = font.measure(text) if first: work_integral = laTeX('$\\sin{(x+\\pi)}$', 'red', k) w.create_text(*A(0, 0), text=text, font=(f_text, k), fill='red', tag='txt', anchor=W) w.create_image(*A(c, -2), image=work_integral, anchor=W, tag='integral') # w.create_text(*A(137, 0), text='.', font=('Avenir Next Ultra Light', k), fill='red') first = False else: img = w.find_withtag('integral') w.coords(img, *A(c, -2)) txt = w.find_withtag('txt') w.itemconfig(txt, text=text) w.update() # time.sleep(0.001) time.sleep(0.5)
def update_font(self): """Update the sidebar text font, usually after config changes.""" font = idleConf.GetFont(self.text, 'main', 'EditorWindow') tk_font = Font(self.text, font=font) char_width = max(tk_font.measure(char) for char in ['>', '.']) self.canvas.configure(width=char_width * 3 + 4) self._update_font(font)
class Scroller(object): """ Scrolls through a solution list. """ def __init__(self, wdw, sols): """ Stores the list of solutions in sols and defines the layout of the GUI. """ wdw.title('solutions scroller') self.sols = sols self.cursor = 0 self.lbl = Label(wdw, text="solution : ") self.lbl.grid(row=0, column=0, sticky=E) self.ent = Entry(wdw) self.ent.grid(row=0, column=1, stick=W) self.ent.insert(INSERT, "0 of %d" % len(sols)) self.myft = Font(family="Courier New", size=12, weight="normal") self.mlen = self.myft.measure("M") lines = sols[0].split('\n') self.width = max([len(line) for line in lines]) self.display = StringVar() self.display.set(self.sols[0]) self.mess = Message(wdw, textvariable=self.display, \ font=self.myft, width=self.width*self.mlen, background='white') self.mess.grid(row=1, column=0, columnspan=2) self.btnext = Button(wdw, command=self.next, text='next') self.btnext.grid(row=2, column=1, sticky=W + E) self.btprev = Button(wdw, command=self.previous, text='previous') self.btprev.grid(row=2, column=0, sticky=W + E) def show(self): """ Shows the solution at position self.cursor in the message widget and updates the entry widget. """ self.display.set(self.sols[self.cursor]) self.ent.delete(0, END) self.ent.insert(INSERT, '%d of %d' % (self.cursor, len(self.sols))) def next(self): """ Increases the cursor by one if possible. """ if self.cursor < len(self.sols) - 1: self.cursor = self.cursor + 1 self.show() def previous(self): """ Decreases the cursor by one if possible. """ if self.cursor > 0: self.cursor = self.cursor - 1 self.show()
class Scroller(object): """ Scrolls through a solution list. """ def __init__(self, wdw, sols): """ Stores the list of solutions in sols and defines the layout of the GUI. """ wdw.title('solutions scroller') self.sols = sols self.cursor = 0 self.lbl = Label(wdw, text="solution : ") self.lbl.grid(row=0, column=0, sticky=E) self.ent = Entry(wdw) self.ent.grid(row=0, column=1, stick=W) self.ent.insert(INSERT, "0 of %d" % len(sols)) self.myft = Font(family="Courier New", size=12, weight="normal") self.mlen = self.myft.measure("M") lines = sols[0].split('\n') self.width = max([len(line) for line in lines]) self.display = StringVar() self.display.set(self.sols[0]) self.mess = Message(wdw, textvariable=self.display, \ font=self.myft, width=self.width*self.mlen, background='white') self.mess.grid(row=1, column=0, columnspan=2) self.btnext = Button(wdw, command=self.next, text='next') self.btnext.grid(row=2, column=1, sticky=W+E) self.btprev = Button(wdw, command=self.previous, text='previous') self.btprev.grid(row=2, column=0, sticky=W+E) def show(self): """ Shows the solution at position self.cursor in the message widget and updates the entry widget. """ self.display.set(self.sols[self.cursor]) self.ent.delete(0, END) self.ent.insert(INSERT, '%d of %d' % (self.cursor, len(self.sols))) def next(self): """ Increases the cursor by one if possible. """ if self.cursor < len(self.sols) - 1: self.cursor = self.cursor + 1 self.show() def previous(self): """ Decreases the cursor by one if possible. """ if self.cursor > 0: self.cursor = self.cursor - 1 self.show()
def label_size(self, label): """Calculate label size""" font_family, font_size = self.font font = Font(family=font_family, size=font_size) width = 0 lines = 0 for line in label.split('\n'): width = max(width, font.measure(line)) lines += 1 xscale = turtle.getscreen().xscale yscale = turtle.getscreen().yscale return width / xscale, font.metrics('linespace') * lines / yscale
def taille_texte(chaine, police='Helvetica', taille='24'): """ Donne la largeur et la hauteur en pixel nécessaires pour afficher ``chaine`` dans la police et la taille données. :param str chaine: chaîne à mesurer :param police: police de caractères (défaut : `Helvetica`) :param taille: taille de police (défaut 24) :return: couple (w, h) constitué de la largeur et la hauteur de la chaîne en pixels (int), dans la police et la taille données. """ font = Font(family=police, size=taille) return font.measure(chaine), font.metrics("linespace")
def __init__(self, master=None): self.theme = THEME_SETUP[THEME] self.font = Font(size=FONT_SIZE) super().__init__(master,bg=self.theme['bg'],width=400,heigh=200,fg=self.theme['fg'],font=self.font,insertbackground=self.theme['cursor']) self.current_open_file = '' self.lastSaved = '' # self.pack(fill=BOTH,expand=1) self.tag_configure("method", foreground="red") self.tag_configure("keyword", foreground="blue") font = Font(font=self['font']) tab = font.measure(' ') self.config(tabs=tab) self.pack_propagate(False) self.grid_propagate(False)
def __config_calendar(self): cols = self._cal.formatweekheader(3).split() self._calendar['columns'] = cols self._calendar.tag_configure('header', background='grey90') self._calendar.insert('', 'end', values=cols, tag='header') # adjust its columns width font = Font() maxwidth = max(font.measure(col) for col in cols) for col in cols: self._calendar.column( col, width=maxwidth, minwidth=maxwidth, anchor='e' )
def create_widgets(self): ''' creates GUI for app ''' # expand widget to fill the grid # self.columnconfigure(1, weight=1, pad=100) # self.rowconfigure(1, weight=1, pad=20) # customize widget style when using ttk... # style = Style() # style.configure("TButton", width=12) self.textedit = Text(self) self.textedit.grid(row=1, column=1, columnspan=3) efont = Font(family="Helvetica", size=14) self.textedit.configure(font=efont) self.textedit.config( wrap=NONE, # wrap = NONE undo=True, # Tk 8.4 width=80, tabs=(efont.measure(' ' * 4), )) self.textedit.focus() ## basic handler commands # # .get("1.0", END) # .delete("1.0", END) # .insert("1.0", "New text content ...") self.scry = Scrollbar(self, orient=VERTICAL, command=self.textedit.yview) self.scry.grid(row=1, column=4, sticky='wns') # use N+S+E self.textedit['yscrollcommand'] = self.scry.set self.scrx = Scrollbar(self, orient=HORIZONTAL, command=self.textedit.xview) self.scrx.grid(row=2, column=1, columnspan=3, sticky='wen') self.textedit['xscrollcommand'] = self.scrx.set btnOpen = Button(self, text='Open', command=self.on_btn_open_clicked) btnOpen.grid(row=3, column=1, pady=8) btnSave = Button(self, text='Save', command=self.on_btn_save_clicked) btnSave.grid(row=3, column=2) btnClose = Button(self, text='Close', command=self.on_btn_close_clicked) btnClose.grid(row=3, column=3)
def drawText(): serial.write("CONF?".encode('utf-8')) query = getMode(serial.read_until()) modeText = query[0] modifierText = query[1] serial.write("READ?".encode('utf-8')) data = serial.read_until() canvas.itemconfigure(value, text=data) canvas.itemconfigure(modifier, text=modifierText) canvas.itemconfigure(mode, text=modeText) font = Font(family="LcdStd", size=60) length = font.measure(data) canvas.coords(modifier, length, 38) canvas.coords(mode, length, 13) serial.write("SYSTEM:LOCAL?".encode('utf-8')) root.after(100, drawText)
treeState[col]['selected'] = False if direction else True treeState[col]['alternate'] = True if direction else False if ttk.Style().theme_use() == 'aque': tree.heading(col, state='user1') treeState[col]['user1'] = True else: tree.heading(col, image=upArrow if direction else downArrow) style = ttk.Style() from tkinter.font import Font font_ = Font(name=style.lookup('Heading', 'font'), exists=True) globals().update(locals()) for col in title: name = col tree.heading(col, text=name, image=noArrow, anchor='w', command=lambda col=col: SortBy(tree, col, False)) tree.column(col, width=font_.measure(name)+noArrow.width()+5) font_ = Font(name=style.lookup('Treeview', 'font'), exists=True) for i in data: tree.insert('', 'end', values=' '.join(i)) for n in i: len_ = font_.measure(col+' ') if tree.column(col, 'width') < len_: tree.column(col, width=len_) ## Code to do the sorting of the tree contents when clicked on ##proc SortBy {tree col direction} { ## # Determine currently sorted column and its sort direction ## foreach c {country capital currency} { ## set s [$tree heading $c state] ## if {("selected" in $s || "alternate" in $s) && $col ne $c} { ## # Sorted column has changed
def create_widgets(self): ''' creates GUI for app ''' # expand widget to fill the grid # self.columnconfigure(1, weight=1, pad=100) # self.rowconfigure(1, weight=1, pad=20) # myfont = Font(family='Lucida Console', weight = 'bold', size = 20) # customize widget style when using ttk... # style = Style() # style.configure("TButton", width=10) # global # style.configure("my.TButton", width=10) # 'style' option ''' ONLY OPTIONS FOR 'grid' FUNCTIONS: column row columnspan rowspan ipadx and ipady padx and pady sticky="nsew" -------------------------------------------------------- ''' self.style = Style() self.style.configure("TButton", width=15) btn = Button(self, text='Close', command=self.exit) btn.grid(row=1, column=1) self.vent = StringVar() # self.vent.trace("w", self.eventHandler) ent = Entry(self, textvariable=self.vent) ent.grid(row=2, column=1) optionlist = ('aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff') self.vopt = StringVar() self.vopt.set(optionlist[0]) opt = OptionMenu(self, self.vopt, *optionlist) opt.grid(row=3, column=1) self.vcombo = StringVar() combo = Combobox(self, textvariable=self.vcombo) combo['values'] = ('value1', 'value2', 'value3') # COMBO.bind('<<ComboboxSelected>>', self.ONCOMBOSELECT) combo.current(0) combo.grid(row=4, column=1) self.vlbl = StringVar() lbl = Label(self, text='', textvariable=self.vlbl) lbl.grid(row=5, column=1) self.vlbl.set('') self.vbar = DoubleVar() bar = Scale(self, variable=self.vbar) bar.grid(row=6, column=1) # str(self.var.get()) self.vchk = IntVar() chk = Checkbutton(self, variable=self.vchk, text='check me out') chk.grid(row=7, column=1) frm = LabelFrame(self, text="label frame", width=100, height=100) frm.grid(row=8, column=1, sticky='nsew') btn2 = Button(frm, text='Hello', command=self.proc) btn2.grid(row=1, column=1) self.vlbl2 = StringVar() lbl2 = Label(frm, text='Hello', textvariable=self.vlbl2) lbl2.grid(row=2, column=1) self.vlbl2.set('Hello') self.txt = Text(self, width=16) self.txt.grid(row=1, column=2, rowspan=6, padx=(5,0), pady=5) efont = Font(family="Helvetica", size=14) self.txt.configure(font=efont) self.txt.config(wrap="word", # wrap=NONE undo=True, # Tk 8.4 height=12, insertbackground='#000', # cursor color tabs=(efont.measure(' ' * 4),)) self.txt.focus() ## basic handler commands # # .get("1.0", END) # .delete("1.0", END) # .insert("1.0", "New text content ...") self.sctxt = Scrollbar(self, orient=VERTICAL, command=self.txt.yview) self.sctxt.grid(row=1, column=3, rowspan=6, sticky='nsw') # use nse self.txt['yscrollcommand'] = self.sctxt.set sep = Separator(self) sep.grid(row=7, column=2, columnspan=4, sticky='ew') menubar = Menu(root) mn_file = Menu(menubar, tearoff=0) mn_file.add_command(label="New", command=self.mn_file_new, accelerator="Ctrl-n", underline=1) mn_file.add_command(label="Open", command=self.nm_file_open) mn_file.add_command(label="Save", command=self.nm_file_save, accelerator="Ctrl-s", underline=1) mn_file.add_command(label="Save-As", command=self.nm_file_saveas) mn_file.add_separator() mn_file.add_command(label="Exit", command=self.nm_file_exit, accelerator="Ctrl-q") menubar.add_cascade(label="File", menu=mn_file) mn_edit = Menu(menubar, tearoff=0) mn_edit.add_command(label="Undo", command=self.mn_edit_undo, accelerator="Ctrl-z") mn_edit.add_command(label="Select All", command=self.mn_edit_selall, accelerator="Ctrl-a") submenu = Menu(mn_edit, tearoff=False) submenu.add_command(label="Copy", command=self.mn_edit_copy, accelerator="Ctrl-c") submenu.add_command(label="Paste", command=self.mn_edit_paste, accelerator="Ctrl-v") mn_edit.add_cascade(label="Clipboard", menu=submenu, underline=2) menubar.add_cascade(label="Edit", menu=mn_edit) mn_help = Menu(menubar, tearoff=0) mn_help.add_command(label="Help Index", command=self.mn_help_index) mn_help.add_command(label="About…", command=self.mn_help_about) menubar.add_cascade(label="Help", menu=mn_help) root.config(menu=menubar) # display the menu self.lst = Listbox(self, width=10) self.lst.grid(row=1, column=4, rowspan=6, sticky='nsew') self.lst.bind("<<ListboxSelect>>", self.on_select_list) for i in range(100): self.lst.insert(i, "Item " + str(i)) # # FUNCS TO EDIT LISTBOX CONTENTS # # def delete_item(self): # if self.listbox.curselection() == (): # return # nothing selected # print("Deleting: " + str(self.listbox.curselection())) # self.listbox.delete(self.listbox.curselection()) # def insert_item(self): # if self.listbox.curselection() == (): # return # nothing selected # list_item = self.listbox.curselection() # self.listbox.insert(list_item[0], self.txtfld.get()) # print("inserted at " + str(list_item[0])) self.sclst = Scrollbar(self, orient=VERTICAL, command=self.lst.yview) self.sclst.grid(row=1, column=5, rowspan=6, sticky='nsw') # use nse self.lst['yscrollcommand'] = self.sclst.set self.vcmbx = StringVar() cmbx = Combobox(self, textvariable=self.vcmbx, width=6) cmbx['values'] = ('alt', 'scidsand', 'classic', 'scidblue', 'scidmint', 'scidgreen', 'default', 'scidpink', 'arc', 'scidgrey', 'scidpurple', 'clam', 'smog', 'kroc', 'black', 'clearlooks', 'radiance', 'blue') cmbx.current(0) cmbx.grid(row=8, column=2, sticky='ew', padx=4) cmbx.bind('<<ComboboxSelected>>', self.comboselected) frm2 = Frame(self) frm2.grid(row=8, column=3, columnspan=3, sticky='nsew') self.vrad = StringVar() # USE ONE VAR PER GROUP OF BUTTONS radyes = Radiobutton(frm2, variable=self.vrad, value='Yes', text='Yes') radyes.grid(row=1, column=1, sticky='ew', padx=10) radno = Radiobutton(frm2, variable=self.vrad, value='No', text='No') radno.grid(row=2, column=1, sticky='ew', padx=10)
# after we figure out how tall the clue will actually be # we will add the clue_rect_default_height = 75 x_pad = 20 # the amount to pad before starting the clue number x_num_pad = 5 # the amount to pad between the clue number and the clue y_pad = 5 # the amount to pad (top and bottom) between the end # of the clue box and the bounding box of the clue text clue_rect_top_y = 0 # zero to start. bottom_y + 1 in the loop for key, value in down_clues.items(): print(key, value) clue_number = key clue_text = value clue_text_length = rfont.measure(clue_text) clue_num_length = rfont.measure(clue_number) clue_text_height = rfont.metrics("linespace") # print("clue_text_length:", clue_text_length) # col_left_x, row_top_y, col_right_x, row_bot_y # clue_rect_bottom_y = clue_rect_top_y + clue_rect_default_height # clue_text_line_len = clue_right_x - (clue_left_x + (x_pad*2) + clue_num_length + x_num_pad + clue_top_y + y_pad) # cr = list_canvas.create_rectangle(clue_left_x, clue_top_y, clue_right_x, clue_bottom_y, fill="light grey") # cn = list_canvas.create_text(clue_left_x + x_pad, clue_top_y + y_pad, text=clue_number, font=bfont, anchor = "nw") # ct = list_canvas.create_text(clue_left_x + x_pad + clue_num_length + x_num_pad, clue_top_y + y_pad, text=clue_text, font=rfont, anchor = "nw", width=clue_text_line_len) # bb = list_canvas.bbox(ct) # x0, y0, x1, y1 = list_canvas.coords(cr) # y1 = bb[3] + y_pad # list_canvas.coords(cr, x0, y0, x1, y1)
class TextEditor(Frame, CaretObserver, TextObserver): def update_text(self, text: str): self.update_status_bar() self.redisplay() def update_caret_location(self, loc: Location): self.update_status_bar() self.redisplay() def __init__(self, text_editor_model: TextEditorModel): super().__init__() # TODO extract self._minimalWidth = 555 self._minimalHeight = 700 self._xMargin = 10 self._numberSpacing = 50 self._yMargin = 20 self._font = Font(size=12, family="Purisa") self._canvasTextColor = flat_colors.FlatUiColors.CLOUDS self._canvasBackgroundColor = flat_colors.FlatUiColors.PETER_RIVER self._canvasHighlightColor = flat_colors.FlatUiColors.EMERALD # TODO extract self._caretShownDelay = 300 self._caretHiddenDelay = 5000 self._caretColor = flat_colors.FlatUiColors.MIDNIGHT_BLUE self.__processed_selection = [] self._model = text_editor_model self._model._caretObservers.add(self) # using an anonymous class: # self._model._observers.add(type("Pero", (CaretObserver, object), # {"updateCaretLocation": lambda _, __: self.redisplay()})()) self._clipboardStack = ClipboardStack() self._pluginsDirectory = "./plugins/" self._pluginsModuleName = "plugins" self._plugins = self._load_plugins() self.init_menubar() self.init_toolbar() self.init_statusbar() self.init_canvas() def _load_plugins(self): plugins = [] for file in os.listdir(self._pluginsDirectory): if file.endswith(".py"): print(file) # try: spec = importlib.util.spec_from_file_location( self._pluginsModuleName, self._pluginsDirectory + file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) for name, obj in inspect.getmembers( module, lambda x: inspect.isclass(x) and issubclass(x, Plugin) and not inspect.isabstract(x)): plugins.append(obj()) # except: # pass return plugins def init_menubar(self): menubar = Menu(self.master) self.master.config(menu=menubar) file_menu = Menu(menubar) file_menu.add_command(label="Open", underline=0, command=self.open_file) file_menu.add_command(label="Save", underline=0, command=self.save_file) file_menu.add_command(label="Exit", underline=0, command=self.on_exit) menubar.add_cascade(label="File", underline=0, menu=file_menu) edit_menu = Menu(menubar) edit_menu.add_command(label="Undo", command=self.undo, state=DISABLED) UndoManager.get_instance().attach_undo_observer( type("UndoManagerStackObserver1", (UndoManagerStackObserver, object), {"stack_empty": lambda _, e: edit_menu.entryconfig(1, state=DISABLED if e else NORMAL)})()) edit_menu.add_command(label="Redo", command=self.redo, state=DISABLED) UndoManager.get_instance().attach_redo_observer( type("UndoManagerStackObserver2", (UndoManagerStackObserver, object), {"stack_empty": lambda _, e: edit_menu.entryconfig(2, state=DISABLED if e else NORMAL)})()) edit_menu.add_command(label="Cut", command=self.cut, state=DISABLED) self._model.attach_selection_observer( type("SelectionObserverCut", (SelectionObserver, object), {"update_selection": lambda _, e: edit_menu.entryconfig(3, state=DISABLED if not e else NORMAL)})()) edit_menu.add_command(label="Copy", command=self.copy, state=DISABLED) self._model.attach_selection_observer( type("SelectionObserverCopy", (SelectionObserver, object), {"update_selection": lambda _, e: edit_menu.entryconfig(4, state=DISABLED if not e else NORMAL)})()) edit_menu.add_command(label="Paste", command=self.paste, state=DISABLED) self._clipboardStack.attach_clipboard_observer( type("ClipboardObserverPaste", (ClipboardObserver, object), {"update_clipboard": lambda _, e: edit_menu.entryconfig(5, state=DISABLED if not e else NORMAL)})()) edit_menu.add_command(label="Paste and Take", command=self.paste_and_take) self._clipboardStack.attach_clipboard_observer( type("ClipboardObserverPasteAndTake", (ClipboardObserver, object), {"update_clipboard": lambda _, e: edit_menu.entryconfig(6, state=DISABLED if not e else NORMAL)})()) edit_menu.add_command(label="Delete selection", command=self.delete_selection, state=DISABLED) self._model.attach_selection_observer( type("SelectionObserverDelete", (SelectionObserver, object), {"update_selection": lambda _, e: edit_menu.entryconfig(7, state=DISABLED if not e else NORMAL)})()) edit_menu.add_command(label="Clear document", command=self.clear_document) menubar.add_cascade(label="Edit", underline=0, menu=edit_menu) move_menu = Menu(menubar) move_menu.add_command(label="Caret to document start", command=self.caret_to_start) move_menu.add_command(label="Caret to document end", command=self.caret_to_end) menubar.add_cascade(label="Move", underline=0, menu=move_menu) plugins_menu = Menu(menubar) for plugin in self._plugins: plugins_menu.add_command( label=plugin.get_name(), command=lambda p=plugin: p.execute(self._model, UndoManager.get_instance(), self._clipboardStack)) menubar.add_cascade(label="Plugins", underline=0, menu=plugins_menu) def init_toolbar(self): toolbar = Frame(self.master) undo = Button(toolbar, text="Undo", command=self.undo, state=DISABLED) undo.pack(side=LEFT, padx=2, pady=2) UndoManager.get_instance().attach_undo_observer( type("UndoManagerStackObserver3", (UndoManagerStackObserver, object), {"stack_empty": lambda _, e: undo.config(state=DISABLED if e else NORMAL)})()) redo = Button(toolbar, text="Redo", command=self.redo, state=DISABLED) redo.pack(side=LEFT, padx=2, pady=2) UndoManager.get_instance().attach_redo_observer( type("UndoManagerStackObserver4", (UndoManagerStackObserver, object), {"stack_empty": lambda _, e: redo.config(state=DISABLED if e else NORMAL)})()) copy = Button(toolbar, text="Copy", command=self.copy, state=DISABLED) copy.pack(side=LEFT, padx=2, pady=2) self._model.attach_selection_observer( type("SelectionObserverCopyTool", (SelectionObserver, object), {"update_selection": lambda _, e: copy.config(state=DISABLED if not e else NORMAL)})()) cut = Button(toolbar, text="Cut", command=self.cut, state=DISABLED) cut.pack(side=LEFT, padx=2, pady=2) self._model.attach_selection_observer( type("SelectionObserverCutTool", (SelectionObserver, object), {"update_selection": lambda _, e: cut.config(state=DISABLED if not e else NORMAL)})()) paste = Button(toolbar, text="Paste", command=self.cut, state=DISABLED) paste.pack(side=LEFT, padx=2, pady=2) self._clipboardStack.attach_clipboard_observer( type("ClipboardObserverPasteTool", (ClipboardObserver, object), {"update_clipboard": lambda _, e: paste.config(state=DISABLED if not e else NORMAL)})()) toolbar.pack(side=TOP, fill=X) def init_statusbar(self): self._statusbar = Label(self.master, bd=1, relief=SUNKEN, padx=6, pady=4, anchor=E) self._statusbar.pack(side=BOTTOM, fill=X) self.update_status_bar() def init_canvas(self): self.master.title("Lab 3 example") self.pack(fill=BOTH, expand=1) self._display_caret = True self._selecting_active = False self._canvas = Canvas(self, bg=self._canvasBackgroundColor, scrollregion=(0, 0, self._minimalWidth, self._minimalHeight)) self._vbar = Scrollbar(self, orient=VERTICAL) self._vbar.pack(side=RIGHT, fill=Y) self._vbar.config(command=self._canvas.yview) self._hbar = Scrollbar(self, orient=HORIZONTAL) self._hbar.pack(side=BOTTOM, fill=X) self._hbar.config(command=self._canvas.xview) self._canvas.config(xscrollcommand=self._hbar.set, yscrollcommand=self._vbar.set) self._canvas.pack(fill=BOTH, expand=1) self.redisplay() self._canvas.bind_all("<Key>", self.on_key_pressed) self._other_dialog_open = False self.after(self._caretShownDelay, self.on_timer) def redisplay(self, update_selection=False): # print("caret loc", self._model.get_caret_location().get(), "||| current row and column", # self._model.find_caret(), "|||| sel", self._model.get_selection_range().get_start(), # self._model.get_selection_range().get_end()) row_height = self._font.metrics()["linespace"] (caret_row, caret_column) = self._model.find_caret() height = max(self._minimalHeight, len( list(self._model.all_lines())) * row_height + 2 * self._yMargin) width = max(self._minimalWidth, self._font.measure( max(list(self._model.all_lines()), key=len)) + self._numberSpacing + 2 * self._xMargin) self._canvas.delete("all") self._canvas.config(scrollregion=(0, 0, width, height)) # TODO follow caret by scrolling left/right and up/down # deltaX = # deltaY = self._render_selection(update_selection) self._render_caret(caret_row, caret_column) for (i, line) in enumerate(self._model.all_lines()): self._canvas.create_text(self._xMargin, self._yMargin + row_height * i, font=self._font, text=str(i), anchor=NW, fill=self._canvasTextColor) self._canvas.create_text(self._xMargin + self._numberSpacing, self._yMargin + row_height * i, font=self._font, text=line, anchor=NW, fill=self._canvasTextColor) def update_selection(self, right_end_moved: bool): """ Update the current selection in the TextEditorModel. If selecting is inactive, the selection will be reset to range [current_caret_location, current_caret_location]. Otherwise, the range will be updated in accordance with the new caret position. :param right_end_moved: was the caret at the right end of the selection """ # TODO move to an more appropriate place new_caret_location = self._model.get_caret_location().get() if self._selecting_active: selection_before = self._model.get_selection_range() if right_end_moved: self._model.set_selection_range(LocationRange(selection_before.get_start().get(), new_caret_location)) else: self._model.set_selection_range(LocationRange(new_caret_location, selection_before.get_end().get())) else: self._model.reset_selection() def on_timer(self): self._display_caret = not self._display_caret self.redisplay(True) self.after(self._caretShownDelay if not self._display_caret else self._caretShownDelay, self.on_timer) def on_exit(self): self.master.destroy() def get_x_y_of_line_start(self, row): return self._xMargin + self._numberSpacing, self._yMargin + self._font.metrics()["linespace"] * row def get_x_y_of_line_end(self, row): return (self._xMargin + self._numberSpacing + self._font.measure(self._model.get_line(row)), self._yMargin + self._font.metrics()["linespace"] * row) def get_x_y_at_caret_location(self, caret_location: Location): row, column = self._model.find_location(caret_location) return self.get_x_y_at_row_and_column(row, column) def get_x_y_at_row_and_column(self, row, column): xy_tuple = self.get_x_y_of_line_start(row) x, y = xy_tuple[0], xy_tuple[1] return x + self._font.measure(self._model.get_line(row)[:column]), y def row_height(self): return self._font.metrics()["linespace"] def _render_caret(self, caret_row, caret_column): color = self._caretColor if self._display_caret else self._canvasBackgroundColor x_start = self._xMargin + self._numberSpacing + self._font.measure( self._model.get_line(caret_row)[:caret_column]) y_start = self._yMargin + self.row_height() * caret_row + 3 self._canvas.create_line(x_start - 3, y_start, x_start + 3, y_start, fill=color) self._canvas.create_line(x_start, y_start, x_start, y_start + self._font.metrics()["linespace"] - 9, fill=color) self._canvas.create_line(x_start - 3, y_start + self._font.metrics()["linespace"] - 9, x_start + 3, y_start + self._font.metrics()["linespace"] - 9, fill=color) def _render_selection(self, update_selection=True): selection = self._model.get_selection_range() if selection.get_start().get() == selection.get_end().get(): return if update_selection: self.__preprocess_selection(selection) for e in self.__processed_selection: self._canvas.create_rectangle(e[0][0], e[0][1], e[1][0], e[1][1] + self.row_height(), outline=self._canvasHighlightColor, fill=self._canvasHighlightColor) def __preprocess_selection(self, selection: LocationRange): sel_start_r, sel_start_c = self._model.find_location(selection.get_start()) sel_end_r, sel_end_c = self._model.find_location(selection.get_end()) if sel_start_r > sel_end_r or sel_start_r == sel_end_r and sel_start_c > sel_end_c: sel_start_r, sel_start_c, sel_end_r, sel_end_c = sel_end_r, sel_end_c, sel_start_r, sel_start_c if sel_start_r == sel_end_r: self.__processed_selection = [(self.get_x_y_at_row_and_column(sel_start_r, sel_start_c), self.get_x_y_at_row_and_column(sel_end_r, sel_end_c))] else: self.__processed_selection = [(self.get_x_y_at_row_and_column(sel_start_r, sel_start_c), self.get_x_y_of_line_end(sel_start_r))] self.__processed_selection += [(self.get_x_y_of_line_start(row), self.get_x_y_of_line_end(row)) for row in range(sel_start_r + 1, sel_end_r)] self.__processed_selection += [(self.get_x_y_of_line_start(sel_end_r), self.get_x_y_at_row_and_column(sel_end_r, sel_end_c))] def update_status_bar(self): ln, col = self._model.find_caret() total_rows = self._model.get_lines_count() self._statusbar['text'] = "Ln: {}, Col: {}\tTotal rows: {}".format(ln, col, total_rows) def on_key_pressed(self, e): if self._other_dialog_open: return keysym = e.keysym char = e.char # https://stackoverflow.com/questions/19861689/check-if-modifier-key-is-pressed-in-tkinter ctrl = (e.state & 0x4) != 0 alt = (e.state & 0x8) != 0 or (e.state & 0x80) != 0 shift = (e.state & 0x1) != 0 self._selecting_active = shift # print(e.keysym, e.char, e.keycode, e.char.isprintable(), "%x" % e.state, ctrl, shift, alt) if keysym == "Left": self._display_caret = True caret_before = self._model.get_caret_location().get() self._model.move_caret_left() self.update_selection(caret_before == self._model.get_selection_range().get_end().get()) elif keysym == "Right": self._display_caret = True caret_before = self._model.get_caret_location().get() self._model.move_caret_right() self.update_selection(caret_before == self._model.get_selection_range().get_end().get()) elif keysym == "Up": self._display_caret = True caret_before = self._model.get_caret_location().get() self._model.move_caret_up() self.update_selection(caret_before == self._model.get_selection_range().get_end().get()) elif keysym == "Down": self._display_caret = True caret_before = self._model.get_caret_location().get() self._model.move_caret_down() self.update_selection(caret_before == self._model.get_selection_range().get_end().get()) elif keysym == "BackSpace": self._display_caret = True if self._model.get_selection_range().is_empty(): action = self._model.execute_delete_before() if action is not None: UndoManager.get_instance().push(action) else: self.delete_selection() elif keysym == "Delete": self._display_caret = True if self._model.get_selection_range().is_empty(): action = self._model.execute_delete_after() if action is not None: UndoManager.get_instance().push(action) else: self.delete_selection() elif keysym == "Return": # TODO what if None action1 = self.delete_selection(False) action2 = self._model.execute_insert_at_caret("\n") actions = [a for a in [action1, action2] if a is not None] if actions: UndoManager.get_instance().push(JumboEditAction(actions)) elif keysym == "Escape": self._display_caret = True self.master.destroy() elif ctrl and keysym.lower() == "c": self.copy() elif ctrl and keysym.lower() == "x": self.cut() elif ctrl and keysym.lower() == "v": if shift: self.paste_and_take() else: self.paste() elif ctrl and keysym.lower() == "y": self.redo() elif ctrl and keysym.lower() == "z": self.undo() elif len(char) and char.isprintable(): # TODO should LocationRange be immutable? Or how should I pass it around? action1 = self.delete_selection(False) action2 = self._model.execute_insert_at_caret(char) self._model.reset_selection() actions = [a for a in [action1, action2] if a is not None] if actions: UndoManager.get_instance().push(JumboEditAction(actions)) def open_file(self): self._other_dialog_open = True try: f = filedialog.askopenfile(mode='r', defaultextension=".txt") if f is None: return text = "\n".join(f.readlines()) self._model.set_text(text) f.close() finally: self._other_dialog_open = False def save_file(self): self._other_dialog_open = True try: f = filedialog.asksaveasfile(mode='w', defaultextension=".txt") if f is None: return text = "\n".join(self._model.all_lines()) f.write(text) f.close() finally: self._other_dialog_open = False def undo(self): um = UndoManager.get_instance() if not um.is_undo_empty(): um.undo() def redo(self): um = UndoManager.get_instance() if not um.is_redo_empty(): um.redo() def copy(self): if not self._model.get_selection_range().is_empty(): self._clipboardStack.push(self._model.get_selected_text()) def cut(self): if not self._model.get_selection_range().is_empty(): self._clipboardStack.push(self._model.get_selected_text()) self.delete_selection() def paste(self): if self._clipboardStack.has_any(): self.delete_selection() action = self._model.execute_insert_at_caret(self._clipboardStack.peek()) if action is not None: UndoManager.get_instance().push(action) def paste_and_take(self): if self._clipboardStack.has_any(): action1 = self.delete_selection(False) action2 = self._model.execute_insert_at_caret(self._clipboardStack.pop()) actions = [a for a in [action1, action2] if a is not None] if actions: UndoManager.get_instance().push(JumboEditAction(actions)) def delete_selection(self, push_action=True) -> EditAction: if self._model.get_selection_range().is_empty(): return sel_left = self._model.get_selection_range().get_left() self._model.move_caret_to(sel_left) action = self._model.execute_delete_range(self._model.get_selection_range().clone()) if push_action and action is not None: UndoManager.get_instance().push(action) self._model.set_selection_range(LocationRange(sel_left.get(), sel_left.get())) return action def clear_document(self): self._model.set_selection_range(LocationRange(0, self._model.get_caret_max())) self.delete_selection() def caret_to_start(self): self._model.move_caret_to(Location(0)) def caret_to_end(self): self._model.move_caret_to(Location(self._model.get_caret_max()))
def Make(self): """ Displays all of this Month's expenses. """ # If the ExpenseFrame exists, it will be destroyed try: self.outer.destroy() except AttributeError as e: if debug: print(e.__class__, ':: ', e) else: pass # outer is created so the delete button frames can be # seperated visually from the expense list frame. self.outer = tk.Frame(self) self.outer.pack() self.Title() self.ExpenseFrame = tk.Frame(self.outer) self.ExpenseFrame.pack(fill='both') # Scrollbar for expense list scrollbar = tk.Scrollbar(self.ExpenseFrame) scrollbar.pack(side='right', fill='y') # Columns for expense list dataCols = ['Date', 'Expense Type', 'Cost', 'Notes'] self.tree = ttk.Treeview(self.ExpenseFrame, columns=dataCols, show='headings') Exp_Attrs = self.master.Budget.expenses.get() # maxWidths is used to store the max width of each column in the # TreeView object. maxWidths = dict() # Loop sets max for each column to 0, so each max has starting value for col in dataCols: maxWidths[col] = 0 # Defines the font that the TreeView elements will use treeFont = Font(self.ExpenseFrame, 'Times', "12") # Inserts each expense into the Treeview object for values in Exp_Attrs: self.tree.insert('', 'end', values=values, tag='expense') # This loop finds the width of the largest string in each column. for col, item in zip(dataCols, values): stringWidth = treeFont.measure(item) if stringWidth > maxWidths[col]: maxWidths[col] = stringWidth # This loop serves two functions: # 1 - Sets the headings in the expense list. # Without this loop, the headings will not actually show. # # 2 - Sets the width of each column based on the largest string within # each column. for col in dataCols: self.tree.heading(col, text=col) extra = 100 MAX = maxWidths[col] + extra self.tree.column(col, width=MAX) # Sets the font of the TreeView elements self.tree.tag_configure('expense', font=treeFont) # 'yscroll' option must be set to scrollbar set object self.tree['yscroll'] = scrollbar.set self.tree.pack(side='left', fill='both') # Associates scrollbar with the Treeview object scrollbar.config(command=self.tree.yview) self.CreateDeleteButton()
class StylableEditor( scrolledtext.ScrolledText ): def __init__(self, parent, **kw): scrolledtext.ScrolledText.__init__(self, parent, **kw) self._myId = None self._cbfnc = None self['wrap'] = tk.WORD self['undo'] = True self.bind('<<TextModified>>', self._onModify) self.bind( '<KeyPress>', self._onKeyPress ) self.bind( '<KeyPress-minus>', self._onHyphen ) self.bind( '<KeyPress-Return>', self._onReturn ) self._indent:bool = False # indentation mode on/off self.bind( '<Control-v>', self._onPaste ) self.isModified:bool = False # define some fonts self._textfont = Font( self, self.cget( "font" ) ) self._boldfont = Font( self, self.cget( "font" ) ) self._boldfont.configure( weight="bold" ) self._italicfont = Font( self, self.cget( "font" ) ) self._italicfont.configure( slant="italic" ) self._bolditalicfont = Font( self, self.cget( "font" ) ) self._bolditalicfont.configure( weight="bold", slant="italic" ) # configure tags self.tag_configure( "bold", font=self._boldfont ) self.tag_configure( "italic", font=self._italicfont ) self.tag_configure( "bold_italic", font=self._bolditalicfont ) self.tag_configure( RED, foreground='red' ) self.tag_configure( GREEN, foreground='green' ) self.tag_configure( BLUE, foreground='blue' ) self.tag_configure( "hyper", foreground="blue", underline=1 ) self.tag_bind( "hyper", "<Enter>", self._enterHyper ) self.tag_bind( "hyper", "<Leave>", self._leaveHyper ) self.tag_bind( "hyper", "<Button-1>", self._clickHyper ) #self.tag_configure( BLACK, foreground='black' ) self._styleRanges:StyleRanges = StyleRanges( self ) #text_font = font.nametofont( self.cget( "font" ) ) bullet_width = self._textfont.measure( "- " ) em = self._textfont.measure( "m" ) self.tag_configure( "indented", lmargin1=em, lmargin2=em + bullet_width ) # https://stackoverflow.com/questions/40617515/python-tkinter-text-modified-callback # create a proxy for the underlying widget self._orig = self._w + "_orig" self.tk.call("rename", self._w, self._orig) self.tk.createcommand(self._w, self._proxy) def _proxy(self, command, *args): cmd = (self._orig, command) + args try: result = self.tk.call(cmd) except: return if command in ("insert", "delete", "replace"): self.event_generate("<<TextModified>>") return result def setModifyCallback(self, cbfnc) -> None: #the given callback function has to take 1 argument: # - evt: Event self._cbfnc = cbfnc def _onModify(self, event ): # triggered by <<TextModified>> virtual event. # the event arg only contains x and y coordinates. # so this method is only used for callback purposes and # to set the isModified flag. self.isModified = True if self._cbfnc: self._cbfnc( event ) def _onKeyPress( self, event ): """ If in indentation mode, check if a char is added to the end of text. If so, expand "indented" range appropriate. Chars inserted in the mid of text will be taken care of by the Text widget itself. """ if not self._indent: return # ignore Backspace and Delete if event.keysym in ( "BackSpace", "Delete" ): return # ignore insertions in the mid of text if not self.index( "end - 1c" ) == self.index( "insert" ): return self.insert( "insert", event.char, "indented" ) return "break" def _onHyphen( self, event ) -> None: """ check if the hyphen was entered in column 0. If so, we switch to indentation mode, where subsequent lines are indented. """ l, c = self.getCaretPosition() if c == 0: self._indent = True print( "indentation mode ON" ) def _onReturn( self, event ) -> None: """Check if in indentation mode. If so, switch it off.""" print( "_onReturn", event ) if event.state == 16: # Return pressed without any other key self._indent = False print( "indentation mode OFF" ) def _onPaste( self, event ): # get the clipboard data, and replace all newlines # with the literal string "\n" clipboard = self.clipboard_get() clipboard = clipboard.replace( "\n", "\\n" ) # delete the selected text, if any try: start = self.index( "sel.first" ) end = self.index( "sel.last" ) self.delete( start, end ) except TclError as e: # nothing was selected, so paste doesn't need # to delete anything pass # insert the modified clipboard contents if self._indent: self.insert( "insert", clipboard, "indented" ) else: self.insert( "insert", clipboard ) #return "break" # prevents Text widget from inserting text from clipboard. def getCaretPosition( self ) -> Tuple[int, int]: """returns line and column index of caret""" position = self.index( 'insert' ) l = int( position.split( '.' )[0] ) c = int( position.split( '.' )[1] ) return l, c def getValue(self) -> any: """ #########comment stackoverflow:############ It is impossible to remove the final newline that is automatically added by the text widget. – Bryan Oakley Jan 13 '18 at 14:09 ==> that's why we do it here: """ s = self.get('1.0', 'end') return s[:-1] def setValue(self, val: str) -> None: self.clear() if val: self.insert('1.0', val) def triggerStyleAction( self, styleAction:StyleAction ) -> None: """ Applies or removes the triggered style to the selected text. """ if styleAction == StyleAction.BOLD: self._handleFontStyle( "bold" ) elif styleAction == StyleAction.ITALIC: self._handleFontStyle( "italic" ) elif styleAction == StyleAction.RED_FOREGROUND: self._handleFontColor( RED ) elif styleAction == StyleAction.BLUE_FOREGROUND: self._handleFontColor( BLUE ) elif styleAction == StyleAction.GREEN_FOREGROUND: self._handleFontColor( GREEN ) elif styleAction == StyleAction.BLACK_FOREGROUND: self._handleFontColor( BLACK ) elif styleAction == StyleAction.MARK_AS_LINK: self._toggleLink() def _handleFontStyle( self, style:str ) -> None: """ # style may be "bold" or "italic". # Basically the style action to take is determined by the first character # in the selected range. If style is set, unset it. If not set, set it. # It's more complicated if e.g. "bold" is to be set and "italic" is already set: # - compare the ranges of the two styles and take proper action, eg: | italic | -> set 4 15 | bold | -> to set according to selection 8 20 ==> from 8 to 15: make italic and bold (tag "bold_italic") ==> from 16 to 20: make bold (tag "bold") """ if style in (BOLD, ITALIC, BOLD_ITALIC): self._toggleFontStyle( style, self.index( "sel.first" ), self.index( "sel.last" ) ) return def _toggleFontStyle( self, style:str, start:str, stop:str ) -> None: if self.isSet( style, start ): self._unsetFontStyle( style, start, stop ) else: self._setFontStyle( style, start, stop ) def isSet( self, style:str, idx:str ) -> bool: # seq = self.tag_nextrange( style, idx, idx ) # returns an empty tuple :-o # Note: even if style is not set, a tuple of len == 1 will be given. # The one and only element of that tuple is the string "None". # That contradicts to the tag_nextrange documentation, which pretends to return an empty string. #return True if len( seq ) > 1 else False stylelist = self.tag_names( idx ) return True if style in stylelist or ( style in (BOLD, ITALIC) and BOLD_ITALIC in stylelist ) else False def _unsetFontStyle( self, style:str, start:str, stop:str ): """ removes the given style within the given range defined by start and stop. Takes style "bold_italic" into account: if bold_italic is set and bold is to be removed, italic will remain. If italic is to be removed, bold will remain. """ """ Processing: Inspect the different style ranges within start-stop. - unset style ranges with matching style - ignore style ranges with different style (except BOLD_ITALIC if style to unset is not BOLD_ITALIC) - BOLD_ITALIC style ranges: remove BOLD_ITALIC and set ITALIC if style to unset is BOLD or set BOLD if style to unset is ITALIC. """ while self.compare( start, "<", stop ): tag = self._styleRanges.getFontStyleAndRange( start, stop ) if tag.name == style: self.tag_remove( style, tag.range.start, tag.range.stop ) elif tag.name == BOLD_ITALIC: self.tag_remove( BOLD_ITALIC, tag.range.start, tag.range.stop ) self.tag_add( BOLD if style == ITALIC else ITALIC, tag.range.start, tag.range.stop ) start = tag.range.stop def _setFontStyle( self, style: str, start: str, stop: str ): """ applies the given style to the given range. start and stop are expected in the form "line.column" If bold is already set and italic is to add remove bold and set BOLD_ITALIC. If italic is already set and bold is to add remove bold and set BOLD_ITALIC. | | -> Range to set style start stop | | | | -> ranges where another style is already set. Replace that style by BOLD_ITALIC """ while self.compare( start, "<", stop ): tag = self._styleRanges.getFontStyleAndRange( start, stop ) if tag.name == "": # no style applied at start. Apply style. self.tag_add( style, tag.range.start, tag.range.stop ) else: # BOLD, ITALIC or BOLD_ITALIC set if tag.name in ( BOLD, ITALIC ): if tag.name != style: # if BOLD or ITALIC, remove it and set BOLD_ITALIC self.tag_remove( tag.name, tag.range.start, tag.range.stop ) #print( "_setFontStyle: adding bold_italic from %s to %s" % (tag.range.start, tag.range.stop) ) self.tag_add( BOLD_ITALIC, tag.range.start, tag.range.stop ) # BOLD_ITALIC can be ignored. start = tag.range.stop def _handleFontColor( self, fontcolor:str ) -> None: self._unsetFontColors( "sel.first", "sel.last" ) if fontcolor != BLACK: self._setFontColor( fontcolor, "sel.first", "sel.last" ) def _setFontColor( self, fontcolor:str, start:str, stop:str ): self.tag_add( fontcolor, start, stop ) def _unsetFontColors( self, start:str, stop:str ): for color in (BLUE, RED, GREEN): try: self.tag_remove( color, start, stop ) except: pass def _toggleLink( self ): start, stop = self._getUrlBoundaries( "insert" ) tag_names = self.tag_names( start ) if "hyper" in tag_names: self.tag_remove( "hyper", start, stop ) else: self.tag_add( "hyper", start, stop ) def _openUrl( self, url ): webbrowser.open_new_tab( url ) def _enterHyper( self, event ): self.config( cursor="hand2" ) def _leaveHyper( self, event ): self.config( cursor="" ) def _clickHyper( self, event ): start, stop = self._getUrlBoundaries( CURRENT ) url = self.get( start, stop ) self._openUrl( url ) def _getUrlBoundaries( self, idx:Any ) -> Tuple[Any, Any]: """ Returns the complete URL starting from index "insert". """ idxA = idxB = self.index( idx ) #search to the left 'til a space, tab or new line if self.compare( idxA, ">", "1.0" ): idxA = self.index( idxA + "-1c" ) c = "" while self.compare( idxA, ">", "1.0" ): c = self.get( idxA, self.index( idxA + "+1c" ) ) if c in (" ", "\n", "\t"): idxA = self.index( idxA + "+1c" ) break idxA = self.index( idxA + "-1c" ) #search to the right 'til a space, tab or new line c = "" while self.compare( idxB, "<", "end" ): c = self.get( idxB, self.index( idxB + "+1c" ) ) if c in ("", "\n", "\t"): break idxB = self.index( idxB + "+1c" ) return (idxA, idxB) def testIterate( self, start, stop ): idx = self.index( start ) stop = self.index( stop ) while self.compare( idx, "<", stop ): print( idx ) idx = self.index( idx + "+1c" ) print( "READY." ) def getStylesAsString( self ) -> str: tagstring:str = "" tagnames = self.tag_names() # get a tuple of tagnames for tagname in tagnames: if tagname != "sel": ranges = self.tag_ranges( tagname ) if len( ranges ) > 0: tagstring += ( tagname + ":" ) for i in range( 0, len( ranges ), 2 ): start:str = str( ranges[i] ) stop:str = str( ranges[i + 1] ) tagstring += (start + "," + stop + ";") tagstring = tagstring[:-1] #remove trailing ";" tagstring += "$" if len( tagstring ) > 0: tagstring = tagstring[:-1] #remove trailing "$" #print( "tagstring: ", tagstring ) return tagstring def setStylesFromString( self, stylesstr:str ) -> None: if not stylesstr: return styles = stylesstr.split( "$" ) # -> bold:1.3,1.6;2.2.,3.4 for style in styles: parts = style.split( ":") #parts[0]: bold parts[1]:1.3,1.6;2.2,3.4 ranges = parts[1].split( ";" ) # -> ranges: 1.2,1.6 for range in ranges: startstop = range.split( "," ) start = startstop[0] stop = startstop[1] self.tag_add( parts[0], start, stop ) def resetModified( self ): self.isModified = False def clear(self) -> None: self.delete('1.0', 'end') def setFont( self, font ): # font example: font = ("Times New Roman", 15) pass def setSelectionFont( self, font ): #font example: font = ("Times New Roman", 15) pass def setSelectionBold( self, on:bool ) -> None: pass def setSelectionItalic( self, on:bool ) -> None: pass
def create_blocks_fst(self): text_height = Font.metrics(self.myFont, 'linespace') top_line = 20 # block for Expr self.canvas.create_polygon(self.command_block_coords( 50, top_line, text_height + 15, 110), fill='violet red', outline='purple', tags='expr_block') self.canvas.create_polygon(self.inside_block_coords( 55, top_line + 10, text_height, 90), fill='light pink', tags='expr_block') top_line += text_height + 25 # block for return command txt_len = Font.measure(self.myFont, 'return') self.canvas.create_polygon(self.command_block_coords( 50, top_line, text_height + 15, 10 + txt_len + 20 + txt_len), fill='violet red', outline='purple', tags='return_block') self.canvas.create_text(57, top_line + 8, anchor=NW, text='return', tags='return_block', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 52 + 1.3 * txt_len, top_line + 10, text_height, txt_len), fill='light pink', tags='return_block') top_line += text_height + 25 # block for variable assignment command txt_len = Font.measure(self.myFont, 'variable') txt2_len = Font.measure(self.myFontBold, '=') self.canvas.create_polygon(self.command_block_coords( 50, top_line, text_height + 15, 10 + txt_len + 15 + txt2_len + 15 + txt_len + 10), fill='violet red', outline='purple', tags='variable_block') self.canvas.create_polygon(self.inside_block_coords( 60, top_line + 10, text_height, txt_len + 5), fill='dodger blue', outline='steel blue', tags='variable_block') self.canvas.create_text(67, top_line + 10, anchor=NW, text="variable", tags='variable_block', font=self.myFont) self.canvas.create_text(67 + txt_len + 8, top_line + 10, anchor=NW, text="=", tags='variable_block', font=self.myFontBold) self.canvas.create_polygon(self.inside_block_coords( 67 + txt_len + 8 + txt2_len + 5, top_line + 10, text_height, txt_len), fill='light pink', tags='variable_block') top_line += text_height + 25 # block for if statement txt_len = Font.measure(self.myFont, 'if') self.canvas.create_polygon(self.control_block_coords( 50, top_line, text_height + 15, 10 + txt_len + 15 + 8 * txt_len + 15, 25)[0], fill='orange', outline='chocolate', tags='if_block') self.canvas.create_polygon(self.control_block_coords( 50, top_line, text_height + 15, 10 + txt_len + 10 + 9 * txt_len + 15, 25)[1], fill='orange', outline='chocolate', tags='if_block') self.canvas.create_text(67, top_line + 10, anchor=NW, text='if', tags='if_block', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 67 + 2 * txt_len, top_line + 9, text_height, 8 * txt_len), fill='peachpuff', tags='if_block') top_line += text_height + 75 # block for while statement txt_len = Font.measure(self.myFont, 'while') self.canvas.create_polygon(self.control_block_coords( 50, top_line, text_height + 15, 10 + txt_len + 15 + txt_len + 25, 25)[0], fill='orange', outline='chocolate', tags='while_block') self.canvas.create_polygon(self.control_block_coords( 50, top_line, text_height + 15, 10 + txt_len + 15 + txt_len + 25, 25)[1], fill='orange', outline='chocolate', tags='while_block') self.canvas.create_text(60, top_line + 10, anchor=NW, text='while', tags='while_block', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 65 + txt_len, top_line + 9, text_height, 55), fill='peachpuff', tags='while_block') top_line += text_height + 75 txt_len = Font.measure(self.myFont, 'print()') self.canvas.create_polygon(self.inside_block_coords( 50, top_line, text_height + 4, txt_len + 30 + txt_len), fill='limegreen', outline='green', tags='print') self.canvas.create_text(65, top_line + 2, anchor=NW, text="print(", tags='print', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 60 + txt_len, top_line + 2, text_height, txt_len), fill='lightgreen', tags='print') self.canvas.create_text(70 + 2 * txt_len, top_line + 2, anchor=NW, text=")", tags='print', font=self.myFont) top_line += text_height + 15 # complete print block self.canvas.create_polygon(self.command_block_coords( 50, top_line, 2 * text_height + 5, 135), fill='violet red', outline='purple', tags='print_complete') self.canvas.create_polygon(self.inside_block_coords( 55, top_line + 10, text_height + 4, txt_len + 30 + txt_len), fill='limegreen', outline='green', tags='print_complete') self.canvas.create_text(65, top_line + 12, anchor=NW, text="print(", tags='print_complete', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 60 + txt_len, top_line + 12, text_height, txt_len), fill='lightgreen', tags='print_complete') self.canvas.create_text(70 + 2 * txt_len, top_line + 12, anchor=NW, text=")", tags='print_complete', font=self.myFont) top_line += text_height + 25 # right side column # equals statement block top_line = 20 txt_len = Font.measure(self.myFontBold, ' == ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 1.2 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='equals') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='equals') self.canvas.create_text(312 + 1.2 * txt_len + 10, top_line + 2, anchor=NW, text="==", tags='equals', font=self.myFontBold) self.canvas.create_polygon(self.inside_block_coords( 310 + 1.2 * txt_len + txt_len + 10, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='equals') top_line += text_height + 15 # not equal statement block txt_len = Font.measure(self.myFontBold, ' != ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 1.5 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='not_equal') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 1.5 * txt_len), fill='sky blue', tags='not_equal') self.canvas.create_text(312 + 1.5 * txt_len + 10, top_line + 2, anchor=NW, text="!=", tags='not_equal', font=self.myFontBold) self.canvas.create_polygon(self.inside_block_coords( 310 + 1.5 * txt_len + txt_len + 10, top_line + 2, text_height, 1.5 * txt_len), fill='sky blue', tags='not_equal') top_line += text_height + 15 # grater than block txt_len = Font.measure(self.myFontBold, ' > ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 2 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='greater') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 2 * txt_len), fill='sky blue', tags='greater') self.canvas.create_text(312 + 2 * txt_len + 10, top_line + 2, anchor=NW, text=">", tags='greater', font=self.myFontBold) self.canvas.create_polygon(self.inside_block_coords( 310 + 2 * txt_len + txt_len + 10, top_line + 2, text_height, 2 * txt_len), fill='sky blue', tags='greater') top_line += text_height + 15 # smaller than block txt_len = Font.measure(self.myFontBold, ' < ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 2 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='smaller') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 2 * txt_len), fill='sky blue', tags='smaller') self.canvas.create_text(312 + 2 * txt_len + 10, top_line, anchor=NW, text="<", tags='smaller', font=self.myFontBold) self.canvas.create_polygon(self.inside_block_coords( 310 + 2 * txt_len + txt_len + 10, top_line + 2, text_height, 2 * txt_len), fill='sky blue', tags='smaller') top_line += text_height + 15 # grater or equal block txt_len = Font.measure(self.myFontBold, ' >= ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 1.2 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='greater_or_equal') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='greater_or_equal') self.canvas.create_text(312 + 1.2 * txt_len + 10, top_line, anchor=NW, text=">=", tags='greater_or_equal', font=self.myFontBold) self.canvas.create_polygon(self.inside_block_coords( 310 + 1.2 * txt_len + txt_len + 10, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='greater_or_equal') top_line += text_height + 15 # smaller or equal block txt_len = Font.measure(self.myFontBold, ' <= ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 1.2 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='smaller_or_equal') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='smaller_or_equal') self.canvas.create_text(312 + 1.2 * txt_len + 10, top_line, anchor=NW, text="<=", tags='smaller_or_equal', font=self.myFontBold) self.canvas.create_polygon(self.inside_block_coords( 310 + 1.2 * txt_len + txt_len + 10, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='smaller_or_equal') top_line += text_height + 15 # or block txt_len = Font.measure(self.myFontBold, ' or ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 1.7 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='or') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 1.7 * txt_len), fill='sky blue', tags='or') self.canvas.create_text(312 + 1.7 * txt_len + 10, top_line, anchor=NW, text="or", tags='or', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 310 + 1.7 * txt_len + txt_len + 10, top_line + 2, text_height, 1.7 * txt_len), fill='sky blue', tags='or') top_line += text_height + 15 # and block txt_len = Font.measure(self.myFont, 'and ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 15 + 2 * 1.2 * txt_len + txt_len + 15), fill='dodger blue', outline='steel blue', tags='and') self.canvas.create_polygon(self.inside_block_coords( 310, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='and') self.canvas.create_text(310 + 1.2 * txt_len + 12, top_line + 2, anchor=NW, text="and", tags='and', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 312 + 1.2 * txt_len + txt_len + 10, top_line + 2, text_height, 1.2 * txt_len), fill='sky blue', tags='and') top_line += text_height + 15 # not block txt_len = Font.measure(self.myFont, 'not ') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height + 4, 10 + txt_len + 10 + 1.6 * txt_len), fill='dodger blue', outline='steel blue', tags='not') self.canvas.create_text(312, top_line + 2, anchor=NW, text='not', tags='not', font=self.myFont) self.canvas.create_polygon(self.inside_block_coords( 312 + txt_len, top_line + 2, text_height, 1.6 * txt_len), fill='sky blue', tags='not') top_line += text_height + 15 # block for variable txt_len = Font.measure(self.myFont, 'variable') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height, txt_len + 20), fill='dodger blue', outline='steel blue', tags='variable') self.canvas.create_text(315, top_line, anchor=NW, text="variable", tags='variable', font=self.myFont) # None block txt_len = Font.measure(self.myFont, 'None') self.canvas.create_polygon(self.inside_block_coords( 400, top_line, text_height, txt_len + 20), fill='dodger blue', outline='steel blue', tags='none') self.canvas.create_text(415, top_line, anchor=NW, text="None", tags='none', font=self.myFont) top_line += text_height + 15 # block for creating number txt_len = Font.measure(self.myFont, 'number') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height, txt_len + 20), fill='dodger blue', outline='steel blue', tags='number') self.canvas.create_text(315, top_line, anchor=NW, text="number", tags='number', font=self.myFont) # True block txt_len = Font.measure(self.myFont, 'True') self.canvas.create_polygon(self.inside_block_coords( 400, top_line, text_height, txt_len + 20), fill='dodger blue', outline='steel blue', tags='true') self.canvas.create_text(415, top_line, anchor=NW, text="True", tags='true', font=self.myFont) top_line += text_height + 15 # block for creating string txt_len = Font.measure(self.myFont, 'string') self.canvas.create_polygon(self.inside_block_coords( 300, top_line, text_height, txt_len + 20), fill='dodger blue', outline='steel blue', tags='string') self.canvas.create_text(315, top_line, anchor=NW, text="string", tags='string', font=self.myFont) # False block txt_len = Font.measure(self.myFont, 'False') self.canvas.create_polygon(self.inside_block_coords( 400, top_line, text_height, txt_len + 25), fill='dodger blue', outline='steel blue', tags='false') self.canvas.create_text(415, top_line, anchor=NW, text="False", tags='false', font=self.myFont)
class Calendar(ttk.Frame): """ Graphical date selection widget, with callbacks. To change the language, use the ``locale`` library with the appropriate settings for the target language. For instance, to display the ``Calendar`` widget in German, you might use:: locale.setlocale(locale.LC_ALL, 'deu_deu') :param parent: the parent frame :param callback: the callable to be executed on selection :param year: the year as an integer, i.e. `2020` :param month: the month as an integer; not zero-indexed; i.e. "1" will translate to "January" :param day: the day as an integer; not zero-indexed :param kwargs: tkinter.frame keyword arguments """ timedelta = datetime.timedelta datetime = datetime.datetime def __init__(self, parent, callback: callable = None, year: int = None, month: int = None, day: int = None, **kwargs): # remove custom options from kw before initializing ttk.Frame fwday = calendar.SUNDAY now = self.datetime.now() year = year if year else now.year month = month if month else now.month day = day if day else now.day locale = kwargs.pop('locale', None) sel_bg = kwargs.pop('selectbackground', '#ecffc4') sel_fg = kwargs.pop('selectforeground', '#05640e') self._date = self.datetime(year, month, day) self._selection = None # no date selected self.callback = callback super().__init__(parent, **kwargs) self._cal = _get_calendar(locale, fwday) self.__setup_styles() # creates custom styles self.__place_widgets() # pack/grid used widgets self.__config_calendar() # adjust calendar columns and setup tags # configure a _canvas, and proper bindings, for selecting dates self.__setup_selection(sel_bg, sel_fg) # store items ids, used for insertion later self._items = [ self._calendar.insert('', 'end', values='') for _ in range(6) ] # insert dates in the currently empty calendar self._build_calendar() def __setitem__(self, item, value): if item in ('year', 'month'): raise AttributeError("attribute '%s' is not writeable" % item) elif item == 'selectbackground': self._canvas['background'] = value elif item == 'selectforeground': self._canvas.itemconfigure(self._canvas.text, item=value) else: ttk.Frame.__setitem__(self, item, value) def __getitem__(self, item): if item in ('year', 'month'): return getattr(self._date, item) elif item == 'selectbackground': return self._canvas['background'] elif item == 'selectforeground': return self._canvas.itemcget(self._canvas.text, 'fill') else: r = ttk.tclobjs_to_py( {item: ttk.Frame.__getitem__(self, item)} ) return r[item] def __setup_styles(self): # custom ttk styles style = ttk.Style(self.master) def arrow_layout(dir): return [ ('Button.focus', { 'children': [('Button.%sarrow' % dir, None)] }) ] style.layout('L.TButton', arrow_layout('left')) style.layout('R.TButton', arrow_layout('right')) def __place_widgets(self): # header frame and its widgets hframe = ttk.Frame(self) lbtn = ttk.Button(hframe, style='L.TButton', command=self._prev_month) rbtn = ttk.Button(hframe, style='R.TButton', command=self._next_month) self._header = ttk.Label(hframe, width=15, anchor='center') # the calendar self._calendar = ttk.Treeview(self, show='', selectmode='none', height=7) # pack the widgets hframe.pack(in_=self, side='top', pady=4, anchor='center') lbtn.grid(in_=hframe) self._header.grid(in_=hframe, column=1, row=0, padx=12) rbtn.grid(in_=hframe, column=2, row=0) self._calendar.pack(in_=self, expand=1, fill='both', side='bottom') def __config_calendar(self): cols = self._cal.formatweekheader(3).split() self._calendar['columns'] = cols self._calendar.tag_configure('header', background='grey90') self._calendar.insert('', 'end', values=cols, tag='header') # adjust its columns width font = Font() maxwidth = max(font.measure(col) for col in cols) for col in cols: self._calendar.column( col, width=maxwidth, minwidth=maxwidth, anchor='e' ) def __setup_selection(self, sel_bg, sel_fg): self._font = Font() self._canvas = canvas = tk.Canvas( self._calendar, background=sel_bg, borderwidth=0, highlightthickness=0 ) canvas.text = canvas.create_text(0, 0, fill=sel_fg, anchor='w') canvas.bind('<ButtonPress-1>', lambda evt: canvas.place_forget()) self._calendar.bind('<Configure>', lambda evt: canvas.place_forget()) self._calendar.bind('<ButtonPress-1>', self._pressed) def __minsize(self, evt): width, height = self._calendar.master.geometry().split('x') height = height[:height.index('+')] self._calendar.master.minsize(width, height) def _build_calendar(self): year, month = self._date.year, self._date.month # update header text (Month, YEAR) header = self._cal.formatmonthname(year, month, 0) self._header['text'] = header.title() # update calendar shown dates cal = self._cal.monthdayscalendar(year, month) for indx, item in enumerate(self._items): week = cal[indx] if indx < len(cal) else [] fmt_week = [('%02d' % day) if day else '' for day in week] self._calendar.item(item, values=fmt_week) def _show_selection(self, text, bbox): """ Configure canvas for a new selection. """ x, y, width, height = bbox textw = self._font.measure(text) canvas = self._canvas canvas.configure(width=width, height=height) canvas.coords(canvas.text, width - textw, height / 2 - 1) canvas.itemconfigure(canvas.text, text=text) canvas.place(in_=self._calendar, x=x, y=y) # Callbacks def _pressed(self, evt): """ Clicked somewhere in the calendar. """ x, y, widget = evt.x, evt.y, evt.widget item = widget.identify_row(y) column = widget.identify_column(x) if not column or not (item in self._items): # clicked in the weekdays row or just outside the columns return item_values = widget.item(item)['values'] if not len(item_values): # row is empty for this month return text = item_values[int(column[1]) - 1] if not text: # date is empty return bbox = widget.bbox(item, column) if not bbox: # calendar not visible yet return # update and then show selection text = '%02d' % text self._selection = (text, item, column) self._show_selection(text, bbox) if self.callback is not None: self.callback() def add_callback(self, callback: callable): """ Adds a callback to call when the user clicks on a date :param callback: a callable function :return: None """ self.callback = callback def _prev_month(self): """ Updated calendar to show the previous month. """ self._canvas.place_forget() self._date = self._date - self.timedelta(days=1) self._date = self.datetime(self._date.year, self._date.month, 1) self._build_calendar() # reconstruct calendar def _next_month(self): """ Update calendar to show the next month. """ self._canvas.place_forget() year, month = self._date.year, self._date.month self._date = self._date + self.timedelta( days=calendar.monthrange(year, month)[1] + 1) self._date = self.datetime(self._date.year, self._date.month, 1) self._build_calendar() # reconstruct calendar @property def selection(self): """ Return a datetime representing the current selected date. """ if not self._selection: return None year, month = self._date.year, self._date.month return self.datetime(year, month, int(self._selection[0]))
def address_UI_init(self): # 0 1 # +------------+--------+ # | menu | # +------------+--------+ # | amt Dashper| | # 0 | balance | QR | # | NEEDED |addr cpy| # +------------+--------+ # 1 | SEND | # +---------------------+ # 2 | Addr search? | # +---------------------+ # 3 | Addrs list | # # # # ====== Dash to send ======= ds_frame = Frame(self) ## ------ amt ------- Label(ds_frame, text='DASH per address:').grid(row=0, column=0) self.amt_per_address = StringVar() self.amt_per_address.set('0.00') Spinbox(ds_frame, textvariable=self.amt_per_address, from_=0.000, to=1000, increment=0.001, width=10, format='%6.8f').grid(row=0, column=1) ## ------ balance self.lb_balance = StringVar() Label(ds_frame, text='Current Balance:').grid(row=1, column=0) Label(ds_frame, textvariable=self.lb_balance).grid(row=1, column=1) self.needed_style = Style() self.needed_style.configure('BW.TLabel', foreground='green') self.lb_needed = StringVar() Label(ds_frame, text='DASH needed:').grid(row=2, column=0) Label(ds_frame, textvariable=self.lb_needed, style='BW.TLabel').grid(row=2, column=1) ds_frame.grid(row=0, column=0) # ====== QR code ======= qr_frame = Frame(self) self.qr_image = PIL.ImageTk.PhotoImage(make_qr_im(self.address)) im_label = Label(qr_frame, compound=BOTTOM, image=self.qr_image) im_label.image = self.qr_image im_label.grid(row=0, column=0, columnspan=3) ## ------- addr ------- self.qr = StringVar() self.qr.set(split_addr(self.address)) qr_label = Label(qr_frame, textvariable=self.qr) qr_label.grid(row=1, column=0, columnspan=2) ## ------ cpy --------- Button(qr_frame, text='Copy', width=4, command=self.address_to_clipboard).grid(row=1, column=2, padx=5) qr_frame.grid(row=0, column=1) # ------ send self.needed_style = Style() self.needed_style.configure('C.TButton', font=('Sans', '10', 'bold')) self.cb_send = Button(self, text='SEND', style='C.TButton', command=self.send) self.cb_send.grid(row=1, column=0, columnspan=2, pady=5) self.cb_send.config(state=DISABLED) # ------ addresses ------- fr = Frame(master=self) fr.grid(sticky=N + E + S + W, row=2, column=0, columnspan=2) # tv_addresses style = Style() style.configure('Treeview', font=('Consolas', 10)) consolas = Font(family='Consolas', size=10) font_w = consolas.measure('m') self.tv_addresses = Treeview(fr, show='tree headings', columns=['Balance']) self.tv_addresses.column('#0', width=38 * font_w, anchor=W, stretch=0) self.tv_addresses.heading('Balance', text='Balance') self.tv_addresses.column('Balance', anchor=E) self.tv_addresses.heading('#0', text='Address') self.tv_addresses.pack(side=LEFT, expand=True, fill='both') sb = Scrollbar(fr) sb.pack(side=RIGHT, fill='y') self.tv_addresses.config(yscrollcommand=sb.set) sb.config(command=self.tv_addresses.yview) # ------ all columns in place so configure them to auto-size Grid.columnconfigure(self, 0, weight=1) Grid.columnconfigure(self, 1, weight=1) Grid.rowconfigure(self, 0, weight=1) Grid.rowconfigure(self, 1, weight=1) Grid.rowconfigure(self, 2, weight=1) Grid.rowconfigure(self, 4, weight=1) # Some events after all variables have been initialised self.amt_per_address.trace('w', self.recalc_needed)
class DNAStrandInterface: ## Valid DNA symbols. symbols = 'atcg' complSymbols = { 'a':'t', 't':'a', 'c':'g', 'g':'c' } def __init__(self, master = None, data1 = "", data2 = ""): self.master = master self.canResize = 0 self.helpIsOpen = False #text fonts self.font = Font(family="Courier", size=22) self.widthOfChar = self.font.measure("a") self.heightOfChar = self.font.metrics("linespace") self.fontHelp = Font(family="Courier", size=12) self.widthOfCharHelp = self.fontHelp.measure("a") self.heightOfCharHelp = self.fontHelp.metrics("linespace") self.canvas = Canvas(master) self.recenter() self.dnaMoving = False self.x = 0 self.y = 0 self.getData() self.movement() #initial scene to choose method of input data def getData(self): self.buttonFile = Button(self.master, text = "Get data from file", command = self.getDataFile) self.buttonType = Button(self.master, text = "Type data", command = self.getDataType) self.buttonFile.place(relx=0.5, rely=0.4, anchor=CENTER) self.buttonType.place(relx=0.5, rely=0.6, anchor=CENTER) self.canvas.pack() #destroys initial scene def destroyInitMenu(self): self.buttonFile.destroy() self.buttonType.destroy() #get input from file #generates error if the path is invalid def getEntryFileData(self): path = self.entryFile.get() try: with open(path, 'r') as f: data1 = f.readline() if(len(data1) >= 1 and data1[len(data1)-1]=='\n'): data1 = data1[:-1] data2 = f.readline() if(len(data2) >= 1 and data2[len(data2)-1]=='\n'): data2 = data2[:-1] f.close() self.destroyDataFile() self.setData(data1, data2) except IOError as e: print("Invalid path") self.canvas.destroy() exit() #destroys data file scene def destroyDataFile(self): self.entryFile.destroy() self.labelFile.destroy() self.buttonGetDataFile.destroy() #creates data file scene def getDataFile(self): self.destroyInitMenu() txt1 = "Path: " self.labelFile = Label(master, text=txt1, font = self.font) self.entryFile = Entry(self.master) self.buttonGetDataFile = Button(self.master, text = "ENTER", command=self.getEntryFileData) self.buttonGetDataFile.place(relx=0.5, rely=0.7, anchor=CENTER) self.labelFile.place(relx=0.4, rely=0.5, anchor=CENTER) self.entryFile.place(relx=0.6, rely=0.5, anchor=CENTER) #creates data type scene def getDataType(self): self.destroyInitMenu() txt1 = "DNA strand 1: " self.labelDna1 = Label(master, text=txt1, font = self.font) txt2 = "DNA strand 2: " self.labelDna2 = Label(master, text=txt2, font = self.font) self.entryDna1 = Entry(self.master) self.entryDna2 = Entry(self.master) self.buttonGetDataType = Button(self.master, text = "ENTER", command=self.getEntryData) self.labelDna1.place(relx=0.4, rely=0.2, anchor=CENTER) self.entryDna1.place(relx=0.7, rely=0.2, anchor=CENTER) self.labelDna2.place(relx=0.4, rely=0.4, anchor=CENTER) self.entryDna2.place(relx=0.7, rely=0.4, anchor=CENTER) self.buttonGetDataType.place(relx=0.5, rely=0.7, anchor=CENTER) self.canvas.pack() #destroys data type scene def destroyDataType(self): self.entryDna1.destroy() self.labelDna1.destroy() self.entryDna2.destroy() self.labelDna2.destroy() self.buttonGetDataType.destroy() #gets input from typing def getEntryData(self): data1 = self.entryDna1.get() data2 = self.entryDna2.get() self.destroyDataType() self.setData(data1, data2) #test if given data is valid def isValid(self, data): if(data == ""): return False valid = True for i in (data): flag = False for j in(self.symbols): if (i == j): flag = True break if flag == False: valid = False break return valid #generates error if given data is invalid def validateData(self, data1, data2): try: if self.isValid(data1) == False: raise ValueError('invalid givenData strand1') if self.isValid(data2) == False: raise ValueError('invalid givenData strand2') except ValueError as error: print(str(error)) self.canvas.destroy() exit() #sets data1 and data2 and creates dna moving scene def setData(self, data1, data2): self.canResize = 0 self.data1 = data1.replace('A','a').replace('T','t').replace('G','g').replace('C','c') self.data2 = data2.replace('A','a').replace('T','t').replace('G','g').replace('C','c') self.validateData(self.data1, self.data2) self.reset() #creates strands with the given data self.strand1 = self.canvas.create_text( self.center['x'], self.center['y'], text = self.data1, font = self.font ) self.strand2 = self.canvas.create_text( self.center['x'] + self.x, self.center['y'] + self.y + self.widthOfChar, text = self.data2, font = self.font) #creates help message self.helpMSG = self.canvas.create_text( 10 + len("h - help")*self.widthOfCharHelp/2.0, 10, text = "h - help", font = self.fontHelp, fill="red") self.helpList = ["Shift-L - shuffle", "Escape - exit", "m - got to position", " of maximum matches", "right key - move right", "left key - move left", "upper key - move up", "down key - move down", "Tab - reset"] self.helpPosX = [ 0, 0, 0, 0,(25*self.widthOfCharHelp), (25*self.widthOfCharHelp), (25*self.widthOfCharHelp), (25*self.widthOfCharHelp), (25*self.widthOfCharHelp)] self.helpPosY = [ 1, 2, 3, 4, 0, 1, 2, 3, 4] self.helpTexts = [] for i in range(len(self.helpList)): self.helpTexts.append(self.canvas.create_text( 10 + len(self.helpList[i])*self.widthOfCharHelp/2.0 + self.helpPosX[i], 10 + self.heightOfCharHelp*self.helpPosY[i] , text = "", font = self.fontHelp)) self.recenter() self.canvas.config(width=2560, height=1536) self.canvas.pack() self.dnaMoving = True #deals with help message def help(self, event): if(self.dnaMoving): msg ="" if(self.helpIsOpen == True): msg = "h - help" for i in range(len(self.helpList)): self.canvas.itemconfig(self.helpTexts[i], text = "") else: msg = "h - close help" for i in range(len(self.helpList)): self.canvas.itemconfig(self.helpTexts[i], text = self.helpList[i]) self.canvas.itemconfig(self.helpMSG, text = msg) self.canvas.coords(self.helpMSG, 10 + len(msg)*self.widthOfCharHelp/2.0, 10) self.helpIsOpen = not self.helpIsOpen #recenters components def recenter(self): self.canvas.pack() self.center = {'y' : self.canvas.winfo_height()/2.0, 'x' : self.canvas.winfo_width()/2.0} #resizes window def resize(self, event): self.width = event.width self.height = event.height if(self.canResize >= 1): self.canvas.config(width=self.width, height=self.height) self.canvas.pack() self.canResize = 0 else: self.canResize += 1 #sets strand2 relative position to the starting position def reset(self): self.x = (len(self.data2) - len(self.data1))*self.widthOfChar/(2.0) self.y = self.heightOfChar def callReset(self, event): if(self.dnaMoving): self.reset() #deals with the strands movements def movement(self): self.recenter() if(self.dnaMoving): self.canvas.coords(self.strand1, self.center['x'], self.center['y']) self.canvas.coords(self.strand2, self.center['x'] + self.x, self.center['y'] + self.y) x_strand2 = self.canvas.coords(self.strand2)[0] y_strand2 = self.canvas.coords(self.strand2)[1] width_strand2 = len(self.data2)*self.widthOfChar height_strand2 = self.heightOfChar if(x_strand2 + width_strand2/2 <= 0 or x_strand2 - width_strand2/2 >= self.canvas.winfo_width()): self.reset() elif(y_strand2 + height_strand2/2 <= 0 or y_strand2 - height_strand2/2 >= self.canvas.winfo_height()): self.reset() self.matches() self.canvas.after(100, self.movement) def left(self, event): self.x += -5 self.y += 0 def right(self, event): self.x += 5 self.y += 0 def up(self, event): self.x += 0 self.y += -5 def down(self, event): self.x += 0 self.y += 5 #returns if math of c1 and c2 is valid def charMatch(self, c1, c2): match = False if c1 in self.symbols: if c2 == self.complSymbols[c1]: match = True # If c1 is in the complement symbols dictionary and c2 is equal to the complement of c1 the match is valid return match def findMatchesWithRightShift(self, shift): myStrand = self.data2 otherStrand = self.data1 mat = [False]*len(myStrand) myIndex = shift # right shift the other is equal to left shift myself otherIndex = 0 # iterates through the letters of the strands testing matches i = 0 while(otherIndex + i < len(otherStrand) and myIndex + i < len(myStrand)): c1 = otherStrand[otherIndex + i] c2 = myStrand[myIndex + i] if (self.charMatch(c1, c2)== True): mat[myIndex + i] = True i+=1 # iterates through the mat array inserting each modified character at its corresponding position in the matches string matches = '' for i in range(len(mat)): if (mat[i]==False): matches += (myStrand[i]).lower() else: matches += myStrand[i].upper() return matches def findMatchesWithLeftShift(self, shift): myStrand = self.data2 otherStrand = self.data1 mat = [False]*len(myStrand) myIndex = 0 otherIndex = shift # iterates through the letters of the strands testing matches i = 0 while(otherIndex + i < len(otherStrand) and myIndex + i < len(myStrand)): c1 = otherStrand[otherIndex + i] c2 = myStrand[myIndex + i] if (self.charMatch(c1, c2) == True): mat[myIndex + i] = True i+=1 # iterates through the mat array inserting each modified character at its corresponding position in the matches string matches = '' for i in range(len(mat)): if (mat[i]==False): matches += (myStrand[i]).lower() else: matches += myStrand[i].upper() return matches def countMatchesWithRightShift(self, shift): myStrand = self.data2 otherStrand = self.data1 count = 0 myIndex = shift # right shift the other is equal to left shift myself otherIndex = 0 # iterates through the letters of the strands testing matches i = 0 while(otherIndex + i < len(otherStrand) and myIndex + i < len(myStrand)): c1 = otherStrand[otherIndex + i] c2 = myStrand[myIndex + i] if (self.charMatch(c1, c2)== True): count += 1 i+=1 return count def countMatchesWithLeftShift(self, shift): myStrand = self.data2 otherStrand = self.data1 count = 0 myIndex = 0 otherIndex = shift # iterates through the letters of the strands testing matches i = 0 while(otherIndex + i < len(otherStrand) and myIndex + i < len(myStrand)): c1 = otherStrand[otherIndex + i] c2 = myStrand[myIndex + i] if (self.charMatch(c1, c2) == True): count += 1 i+=1 return count def findMaxPossibleMatches(self, event): if(self.dnaMoving): countMax = -1 posMax = -1 limitShiftLeft = len(self.data1) - 1 limitShiftRight = len(self.data2) - 1 for shift in range(limitShiftLeft): matches = self.countMatchesWithLeftShift(shift) if (matches > countMax): countMax = matches posMax = shift for shift in range(limitShiftRight): matches = self.countMatchesWithRightShift(shift) if (matches> countMax): countMax = matches posMax = -shift self.reset() self.x += posMax*self.widthOfChar # tests matches and indicates them with uppercase letters def matches(self): self.canvas.pack() ini_x_strand2 = self.center['x'] + (len(self.data2) - len(self.data1))*self.widthOfChar/(2.0) x_strand2 = self.canvas.coords(self.strand2)[0] if(x_strand2 >= ini_x_strand2): shift = (int) (x_strand2 - ini_x_strand2) // self.widthOfChar self.canvas.itemconfig(self.strand2, text = self.findMatchesWithLeftShift(shift)) else: shift = (int) (ini_x_strand2 - x_strand2) // self.widthOfChar self.canvas.itemconfig(self.strand2, text = self.findMatchesWithRightShift(shift)) #shuffles letters of strand2 def shuffle(self, event): if(self.dnaMoving): newData = list(self.data2) for i in range(0,len(newData) - 1): j = randint(i+1, len(newData)-1) newData[i], newData[j] = newData[j], newData[i] self.data2 = ''.join(newData) self.canvas.itemconfig(self.strand2, text = self.data2) #exits application def quit(self, event): self.canvas.destroy() exit()
class CanvasElem(object): '''CanvasElem keeps track of a canvas element, possibly with text.''' STD_FONTS = [ {'family':'Bookman Old Style', 'size':14},\ {'family':'Century', 'size':14}, \ {'family':'Courier', 'size':14}] #Changes the script-name of the gate to the display-name of the gate GATE_MASKS = {'h':'H', 'x':'X', 'y':'Y', 'z':'Z', 'rx':'Rx', 'ry':'Ry',\ 'rz':'Rz', 's':'S', 'ph':'S','t':'T', 'tdag':'T^', 'measure':'M', 'prepz':'|0>'} #All the gates that have a special drawing style SPECIAL_GATES = ('cnot', 'cx', 'toffoli', 'swap', 'cphase', 'cz', 'cr','c-x','c-z','class_cx','class_cz') SPECIAL_NODES = ('circ','oplus','cross') #Determine the radius of the associated nodes of the special gates RADII = {'circ':5, 'oplus': 9, 'cross':6 } #Determine the width of the boxes around the gates BORDER_WIDTH = 3 #Determine the margins around the gates. MARGINS = (0,0) #(5,5) def __init__(self, canvas, gate=None, aspect = (-1,-1), bbox=None, font_dict=None,\ special_node = None, draw_rect = True): self.canvas = canvas self.aspect = aspect self.bbox = bbox #Keep track of the actual drawing coordinates self.draw_x = -1 self.draw_y = -1 self.draw_w = -1 self.draw_h = -1 self.text_x = -1 self.text_y = -1 #Keep track of the min dimensions of the bbox such that the contents can be displayed correctly self.min_w = -1 self.min_h = -1 #Keep track of the attachment points to which other elements can attach themselves. self.attachments = {'left':-1, 'right':-1, 'top':-1, 'bottom':-1} #Keep track of the text within the rectangle self.text = None #Set the font self.font = None if font_dict: self.font = Font(family=font_dict['family'], size=font_dict['size']) else: for font_dict in self.STD_FONTS: try: self.font = Font(family=font_dict['family'],size=font_dict['size']) if self.font: break except Exception as e: pass else: raise ValueError(f'{self.__str__()} cannot produce any font, none worked!') #Keep track of the canvas elements self.rect_canvas = None self.text_canvas = None self.special_node = special_node self.specials_canvas = [] #Keep track of whether we need to draw the rectangle self.draw_rect = draw_rect #Update the current gate, and the associated text self.set_gate(gate) def find_min_size(self): '''Finds the minimum size of the rectangle needed to contain the text''' if self.special_node: #self.min_w = max(2 * self.RADIUS, self.font.measure('x'), self.font.measure('o')) #self.min_h = max(2 * self.RADIUS, self.font.metrics('linespace')) self.min_w = self.min_h = 2 * self.RADII[self.special_node] #If we are an element with actual text inside, compute how large the text is elif self.text: min_w = self.font.measure(self.text) + self.draw_rect*self.BORDER_WIDTH*2 min_h = self.font.metrics('linespace') + self.draw_rect*self.BORDER_WIDTH*2 #Take into account the aspect ratio in self.aspect if not -1 in self.aspect: #We must have w/h = aspect[0]/aspect[1] => w = h * aspect[0]/aspect[1] #Either stretch w, or stretch h if min_h * self.aspect[0]/self.aspect[1] > min_w: self.min_w = int( min_h * self.aspect[0]/self.aspect[1] ) self.min_h = int(min_h) else: self.min_w = int(min_w) self.min_h = int( min_w * self.aspect[1]/self.aspect[0] ) else: self.min_w = int(min_w) self.min_h = int(min_h) def set_bbox(self,bbox): self.bbox = bbox def set_gate(self,gate): self.gate = gate if self.gate in self.GATE_MASKS.keys(): self.text = self.GATE_MASKS[self.gate] #If the gate is a special gate, suppress the text. Otherwise, set it. elif self.gate in self.SPECIAL_GATES: self.text = None else: self.text = self.gate self.find_min_size() def find_draw_coords(self): '''Finds the coordinates to draw with, and sets up the attachment points''' if self.bbox is None: raise ValueError(f'{self.__str__()} cannot find draw coords because bbox is None!') #Do NOT include margins if we are building a special node if self.special_node: want_width = self.min_w want_height = self.min_h else: want_width = self.min_w + 2 * self.MARGINS[0] want_height = self.min_h + 2 * self.MARGINS[1] self.draw_w = want_width if want_width < self.bbox['w'] else self.bbox['w'] self.draw_h = want_height if want_height < self.bbox['h'] else self.bbox['h'] self.draw_x = int( self.bbox['x'] + (self.bbox['w'] - self.draw_w)/2 ) self.draw_y = int( self.bbox['y'] + (self.bbox['h'] - self.draw_h)/2 ) self.text_x = int( self.draw_x + self.draw_w/2 ) self.text_y = int( self.draw_y + self.draw_h/2 ) self.attachments['left'] = self.draw_x self.attachments['right'] = self.draw_x + self.draw_w self.attachments['top'] = self.draw_y self.attachments['bottom'] = self.draw_y + self.draw_h def draw(self): '''Draws the element on the canvas''' #First, find the coords at which we should draw. self.find_draw_coords() #If we are a normal node: if self.special_node is None: #If we should draw a rectangle: if self.draw_rect: self.rect_canvas = self.canvas.create_rectangle(self.draw_x,self.draw_y,\ self.draw_x+self.draw_w,self.draw_y+self.draw_h,width=self.BORDER_WIDTH) #If we are a measurement device if self.gate == 'measure': self.specials_canvas += self.draw_measurement() #If we have text: elif self.text: self.text_canvas = self.canvas.create_text(self.text_x,self.text_y,font=self.font, justify=tk.CENTER,\ text=self.text) else: #We are special: we need to draw either a circ, an oplus or a cross def node(xy, r, circ=False, fill=False, plus=False, cross=False): out = [] if circ: out.append(self.canvas.create_oval( xy[0]-r, xy[1]-r, xy[0]+r, xy[1]+r, fill='black' if fill else '', width=1.5) ) if plus: out.append(self.canvas.create_line( xy[0], xy[1]-r, xy[0], xy[1]+r, width=2 ) ) out.append(self.canvas.create_line( xy[0]-r, xy[1], xy[0]+r, xy[1], width=2 ) ) if cross: out.append(self.canvas.create_line( xy[0]-r, xy[1]-r, xy[0]+r, xy[1]+r, width=2.5 ) ) out.append(self.canvas.create_line( xy[0]-r, xy[1]+r, xy[0]+r, xy[1]-r, width=2.5 ) ) return out mid_x = int( self.bbox['x'] + self.bbox['w']/2 ) mid_y = int( self.bbox['y'] + self.bbox['h']/2 ) if self.special_node == 'circ': self.specials_canvas += node((mid_x,mid_y), self.RADII['circ'], circ=True, fill=True ) elif self.special_node == 'oplus': self.specials_canvas += node((mid_x,mid_y), self.RADII['oplus'], circ=True, plus=True) else: self.specials_canvas += node((mid_x,mid_y), self.RADII['cross'], cross=True) def draw_measurement(self): '''Draws a measurement device''' mid_x = int(self.draw_x + self.draw_w/2) mid_y = int(self.draw_y + 3*self.draw_h/5) radius = int( (self.draw_w/2) * 7/10 ) arc = self.canvas.create_arc( mid_x-radius, mid_y-radius, mid_x+radius, mid_y+radius,\ start=0, extent=180, width=2, style=tk.ARC ) end_x = int(self.draw_x + self.draw_w * 8.5/10 ) end_y = int(self.draw_y + self.draw_h * 1.5/10 ) arrow = self.canvas.create_line(mid_x, mid_y, end_x, end_y, arrow=tk.LAST, width=2 ) return arc, arrow def __str__(self): return f'R.D. DRAW(x={self.draw_x},y={self.draw_y},w={self.draw_w},h={self.draw_h},text={self.text})' def __repr__(self): return self.__str__()
class UITabs(Frame): def __init__(self, parent, observer): Frame.__init__(self, parent) self.parent = parent self.observer = observer self.tabs = [] # list of tab id's self.info = {} # info on each tab self.drag = None # state information when drag in progress self.current = None # id of currently selected tab self.mouseover = None # id of tab the mouse is currently over self.tooltip = None # state information for tooltips self.last_x = -1 self.nextid = 1 self.define_appearance() self.c = Canvas(self, highlightthickness=0, borderwidth=0) self.c.grid(column=0, row=0, sticky='nwes') self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) self.c['background'] = self.bg self.c['height'] = self.height self.bind('<Configure>', self.rebuild) self.c.bind('<Motion>', self.mouse_moved) self.c.bind('<Leave>', self.leave) self.c.bind('<1>', self.mouse_down) self.c.bind('<B1-Motion>', self.mouse_dragged) self.c.bind('<ButtonRelease-1>', self.mouse_up) def add(self, tabid=None, title=None, dirty=False, tooltip=None): "Add a new tab" if tabid is None: while 'tab'+str(self.nextid) in self.tabs: self.nextid += 1 tabid = 'tab'+str(self.nextid) self.nextid += 1 if tabid in self.tabs: raise ValueError('tab already added') self.tabs.append(tabid) self.info[tabid] = {'title': title, 'dirty': dirty, 'tooltip': tooltip} self.select(tabid) return tabid def remove(self, tabid): "Remove an existing tab" if tabid not in self.tabs: raise ValueError('no such tab') self.tooltip_clear() idx = self.tabs.index(tabid) del(self.info[tabid]) self.tabs.remove(tabid) if tabid == self.current: if len(self.tabs) == 0: self.current = None elif idx > len(self.tabs)-1: self.current = self.tabs[-1] else: self.current = self.tabs[idx] if self.current is not None: self.observer.tab_selected(self, self.current) self.update_mouseover(self.last_x) self.rebuild() def select(self, tabid): "Change the currently selected (frontmost) tab." if tabid not in self.tabs: raise ValueError('no such tab') if self.current != tabid: self.observer.tab_deselected(self, self.current) self.current = tabid self.rebuild() self.observer.tab_selected(self, self.current) def set_title(self, tabid, title): "Change the title of a tab" if tabid not in self.tabs: raise ValueError('no such tab') self.info[tabid]['title'] = title self.rebuild() def set_dirty(self, tabid, dirty=True): "Change the saved/unsaved indicator for a tab" if tabid not in self.tabs: raise ValueError('no such tab') self.info[tabid]['dirty'] = dirty self.rebuild() def set_tooltip(self, tabid, tooltip): "Change the tooltip for a tab" if tabid not in self.tabs: raise ValueError('no such tab') self.info[tabid]['tooltip'] = tooltip def mouse_moved(self, ev): self.last_x = ev.x self.update_mouseover(ev.x) def leave(self, ev): self.mouseover = None self.tooltip_clear() self.last_x = -1 self.rebuild() def mouse_down(self, ev): self.drag_initiate(ev) def mouse_dragged(self, ev): self.drag_continue(ev) def mouse_up(self, ev): self.drag_conclude(ev) def define_appearance(self): self.height = 22 self.minwidth = 130 self.maxwidth = 300 self.addiconwidth = 24 self.gapwidth = 15 self.bg = '#c4c4c4' self.selbg = '#d3d3d3' self.dividerbg = '#b0b0b0' self.textcolor = '#424242' self.holecolor = '#eeeeee' self.titlefont = Font(name='TkTooltipFont', exists=True, root=self.parent) self.tooltipfont = Font(name='TkTooltipFont', exists=True, root=self.parent) self.plusfont = Font(name='TkMenuFont', exists=True, root=self.parent) def calculate_positions(self): "Update state info on each tab needed for display, e.g. position" self.tabwidth = tabwidth = self.calculate_tabwidth() displayed = [] extra = [] xpos = 0 # Step 1: Calculate positions for each tab that will fit on the # display, and note which tabs won't fit for t in self.tabs: if xpos + tabwidth > self.winfo_width() - self.addiconwidth: self.info[t]['visible'] = False extra.append(t) else: self.info[t]['visible'] = True self.info[t]['left'] = xpos self.info[t]['shift'] = 0 displayed.append(t) xpos += tabwidth self.info[t]['tablabel'] = self.tablabel(self.info[t]['title'], tabwidth-40) # Step 2: If we're in the middle of dragging a tab, potentially # to move it, we indicate a place where it can be dropped # via a gap between tabs. If such a gap has been identified # (see drag_identifygap), open up a space by moving all tabs # to the left of the gap a bit more left, and all tabs to # the right of the gap a bit more right. if self.drag is not None and self.drag['state'] == 'inprogress' \ and self.drag['gap'] is not None: gap = self.drag['gap'] tabidx = self.tabs.index(self.drag['tabid']) idx = 0 for t in self.tabs: if self.info[t]['visible']: if idx < gap: self.info[t]['shift'] = -self.gapwidth elif idx >= gap: self.info[t]['shift'] = +self.gapwidth idx += 1 # Step 3: If the currently selected tab will not fit on the screen # because there are too many tabs before it on the list, # swap the last displayed tab with the currently selected # one, to ensure it is displayed. Note this doesn't update # the actual order (in self.tabs), just display state info. if self.current in extra: last = displayed[-1] self.info[self.current]['visible'] = True self.info[self.current]['left'] = self.info[last]['left'] self.info[self.current]['shift'] = self.info[last]['shift'] self.info[last]['visible'] = False del(self.info[last]['left']) del(self.info[last]['shift']) displayed.remove(last) extra.insert(0, last) extra.remove(self.current) displayed.append(self.current) self.overflow_tab = displayed[-1] if extra else None self.plus_x = xpos def calculate_tabwidth(self): "Determine width of a tab, factoring in available space, etc." fullwidth = self.winfo_width() numtabs = len(self.tabs) if numtabs == 0: return -1 tabwidth = int((fullwidth - self.addiconwidth) / numtabs) if tabwidth < self.minwidth: tabwidth = self.minwidth if tabwidth > self.maxwidth: tabwidth = self.maxwidth return tabwidth def tablabel(self, title, width): "Truncate any long titles that would not fit within the tab label" if self.titlefont.measure(title) <= width: return title dotwidth = self.titlefont.measure('...') while True: if self.titlefont.measure(title+'...') <= width: return title+'...' title = title[:-1] def update_mouseover(self, x): "Determine if the tab our mouse is over has changed, and adjust if so" nowover = self.tabunder(x) if x != -1 else None if nowover != self.mouseover: self.tooltip_clear() self.mouseover = nowover self.rebuild() if nowover is not None and \ self.info[nowover]['tooltip'] is not None: self.tooltip_schedule() def tabunder(self, x): "Identify which tab is at a given position" for t in self.tabs: if self.info[t]['visible'] and self.info[t]['left'] <= x <\ self.info[t]['left'] + self.tabwidth: return t return None def rebuild(self, ev=None): """ Update the display to match the current state of the user interface. This actually draws all the pieces of the tabs, indicators, etc. on the canvas. We take a brute force approach and recreate everything from scratch each time we're called, which happens on any significant state change. """ self.c.delete('all') if self.winfo_width() < self.addiconwidth: return self.calculate_positions() tabwidth = self.tabwidth for t in self.tabs: if not self.info[t]['visible']: continue lbl = self.info[t]['tablabel'] xpos = self.info[t]['left'] + self.info[t]['shift'] color = self.selbg if t == self.current else self.bg rect = self.c.create_rectangle(xpos, 0, xpos + tabwidth - 2, self.height, fill=color, outline=color) self.c.tag_bind(rect, '<ButtonRelease-1>', lambda ev=None, t=t: self.select(t)) self.c.create_line(xpos-1, 0, xpos-1, self.height, fill=self.dividerbg) self.c.create_line(xpos+tabwidth-1, 0, xpos+tabwidth-1, self.height, fill=self.dividerbg) color = self.textcolor if self.drag is not None and self.drag['state'] == 'inprogress' \ and self.drag['tabid'] == t: color = self.holecolor txt = self.c.create_text(xpos + tabwidth / 2, self.height - 3, anchor='s', text=lbl, fill=color, font=self.titlefont) self.c.tag_bind(txt, '<ButtonRelease-1>', lambda ev=None, t=t: self.select(t)) if self.info[t]['dirty']: close = self.c.create_oval(xpos+4, self.height-13, xpos+10, self.height-7, fill=self.textcolor, outline=self.textcolor, activefill='red', activeoutline='red') self.c.tag_bind(close, '<1>', lambda ev=None, t=t: self.closetab(t, ev)) elif t == self.mouseover: if self.drag is None or self.drag['state'] != 'inprogress': close = self.c.create_text(xpos+8, self.height-3, anchor='s', text='x', fill=self.textcolor, activefill='red', font=self.plusfont) self.c.tag_bind(close, '<ButtonRelease-1>', lambda ev=None, t=t: self.closetab(t, ev)) if t == self.overflow_tab: more = self.c.create_text(xpos + tabwidth - 12, self.height - 5, anchor='s', text='>>', fill=self.textcolor, font=self.titlefont) self.c.tag_bind(more, '<1>', self.post_extras_menu) plus = self.c.create_text(self.plus_x+self.addiconwidth/2, self.height-3, anchor='s', text='+', fill=self.textcolor, font=self.plusfont) self.c.tag_bind(plus, '<ButtonRelease-1>', self.addtab) self.drag_createitems() def addtab(self, ev=None): self.observer.handle_addtab(self) def closetab(self, tabid, ev): self.observer.handle_closetab(self, tabid) def post_extras_menu(self, ev): "Show a menu for selecting the tabs that do not fit onscreen" self.tooltip_clear() self.calculate_positions() menu = Menu(self, tearoff=0) for t in self.tabs: if not self.info[t]['visible']: menu.add_command(label=self.info[t]['title'], command=lambda t=t: self.select(t)) menu.tk_popup(ev.x_root, ev.y_root) def drag_initiate(self, ev): tabid = self.tabunder(ev.x) if tabid is not None: self.drag = {'state': 'pending', 'tabid': tabid, 'offsetx': ev.x - (self.info[tabid]['left'] + self.tabwidth/2), 'offsety': ev.y - (self.height - 3), 'x': ev.x, 'y': ev.y} def drag_continue(self, ev): if self.drag is None: return if self.drag['state'] == 'pending': if abs(ev.x - self.drag['x']) > 3 or \ abs(ev.y - self.drag['y']) > 3: self.drag['state'] = 'inprogress' self.drag['x'] = ev.x - self.drag['offsetx'] self.drag['y'] = ev.y - self.drag['offsety'] self.drag['gap'] = None self.rebuild() elif self.drag['state'] == 'inprogress': self.drag['x'] = ev.x - self.drag['offsetx'] self.drag['y'] = ev.y - self.drag['offsety'] self.c.coords(self.drag['textitem'], (self.drag['x'], self.drag['y'])) self.drag_identifygap(ev.x, ev.y) def drag_conclude(self, ev): if self.drag is not None and self.drag['state'] == 'inprogress': self.drag_identifygap(ev.x, ev.y) gap = self.drag['gap'] if gap is not None: curidx = self.tabs.index(self.drag['tabid']) if gap > curidx: gap -= 1 self.tabs.remove(self.drag['tabid']) self.tabs.insert(gap, self.drag['tabid']) self.drag = None self.rebuild() def drag_createitems(self): "Called by rebuild to create canvas items for drag in progress" if self.drag is not None and self.drag['state'] == 'inprogress': if self.drag['gap'] is not None: x = self.drag['gap'] * self.tabwidth self.c.create_rectangle(x-self.gapwidth, 0, x+self.gapwidth, self.height, fill=self.holecolor, outline=self.holecolor) label = self.info[self.drag['tabid']]['tablabel'] self.drag['textitem'] = self.c.create_text(self.drag['x'], self.drag['y'], text=label, font=self.titlefont, fill=self.textcolor, anchor='s') def drag_identifygap(self, x, y): gap = None for t in self.tabs: if not self.info[t]['visible']: continue left = self.info[t]['left'] if left <= x <= left + self.tabwidth / 2: gap = self.tabs.index(t) break elif left + self.tabwidth / 2 <= x <= left + self.tabwidth: gap = self.tabs.index(t) + 1 break tabidx = self.tabs.index(self.drag['tabid']) if gap is not None and (tabidx == gap or tabidx + 1 == gap): gap = None if y < 0 or y > self.height: gap = None if gap != self.drag['gap']: self.drag['gap'] = gap self.rebuild() def tooltip_schedule(self): self.tooltip_clear() self.tooltip = {'window': None, 'afterid': self.after(1500, self.tooltip_display)} def tooltip_clear(self): if self.tooltip is not None: if self.tooltip['window'] is not None: self.tooltip['window'].destroy() if self.tooltip['afterid'] is not None: self.after_cancel(self.tooltip['afterid']) self.tooltip = None def tooltip_display(self): self.tooltip['afterid'] = None if self.mouseover is None: return x = self.winfo_rootx() + self.info[self.mouseover]['left'] + 20 y = self.winfo_rooty() + self.height + 5 txt = self.info[self.mouseover]['tooltip'] tw = self.tooltip['window'] = Toplevel(self) tw.wm_withdraw() tw.wm_geometry("+%d+%d" % (x, y)) tw.wm_overrideredirect(1) try: tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "noActivates") except TclError: pass lbl = Label(tw, text=txt, justify=LEFT, background="#ffffe0", borderwidth=0, font=self.tooltipfont) if self.tk.call('tk', 'windowingsystem') != 'aqua': lbl['borderwidth'] = 1 lbl['relief'] = 'solid' lbl.pack() tw.update_idletasks() # calculate window size to avoid resize flicker tw.deiconify() tw.lift() # needed to work around bug in Tk 8.5.18+ (issue #24570)
def __init__(self, parent, clue_direction, clue_list, clue_list_box_height, **kwargs): super().__init__(parent, **kwargs) self.clue_direction = clue_direction self.list_label = tk.Label(self, text=self.clue_direction, fg="black", bg="white", anchor="w") self.list_label.pack(side=tk.TOP, fill=tk.X, expand=1) self.list_canvas = tk.Canvas(self, bg='#FFFFFF', width=240, height=clue_list_box_height, scrollregion=(0, 0, 240, 1500)) #vbar=tk.Scrollbar(self, orient=tk.VERTICAL) vbar = tk.Scrollbar(self, orient=tk.VERTICAL, bg="white") vbar.pack(side=tk.RIGHT, fill=tk.Y) vbar.config(command=self.list_canvas.yview) self.list_canvas.config(yscrollcommand=vbar.set) # list_canvas.config(width=240,height=300, yscrollcommand=vbar.set) self.list_canvas.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) ##yscrollincrement - increment for vertical scrolling, in pixels, ##millimeters '2m', centimeters '2c', or inches '2i' bfont = Font(family="Arial", size=10, weight="bold") rfont = Font(family="Arial", size=10) # Because our list is fixed in width # the x coords are going to be constant clue_rect_left_x = 0 clue_rect_right_x = 240 # total width of the clue rectangle # default clue rectangle height: this will change # after we figure out how tall the clue will actually be. clue_rect_default_height = 75 x_pad = 30 # the amount to pad before starting the clue number x_num_pad = 5 # the amount to pad between the clue number and the clue y_pad = 5 # the amount to pad (top and bottom) between the end # of the clue box and the bounding box of the clue text clue_rect_top_y = 0 # zero to start. bottom_y + 1 in the loop for clue_number, clue_text in clue_list: clue_num_length = rfont.measure(clue_number) clue_rect_bottom_y = clue_rect_top_y + clue_rect_default_height clue_text_line_len = clue_rect_right_x - ( (x_pad * 2) + clue_num_length + x_num_pad) clue_num_left_x = clue_rect_left_x + (x_pad - clue_num_length) clue_num_top_y = clue_rect_top_y + y_pad clue_text_left_x = clue_num_left_x + clue_num_length + x_num_pad clue_text_top_y = clue_rect_top_y + y_pad clue_tag = self.clue_direction[0] + str(clue_number).zfill(3) cr = self.list_canvas.create_rectangle(clue_rect_left_x, clue_rect_top_y, clue_rect_right_x, clue_rect_bottom_y, fill="white", tags=clue_tag) cn = self.list_canvas.create_text(clue_num_left_x, clue_num_top_y, text=clue_number, font=bfont, anchor="nw", tags=clue_tag) ct = self.list_canvas.create_text(clue_text_left_x, clue_text_top_y, text=clue_text, font=rfont, anchor="nw", width=clue_text_line_len, tags=clue_tag) bb = self.list_canvas.bbox(ct) x0, y0, x1, y1 = self.list_canvas.coords(cr) y1 = bb[3] + y_pad self.list_canvas.coords(cr, x0, y0, x1, y1) clue_rect_top_y = y1 self.list_canvas.itemconfig(cr, outline="white") self.list_canvas.configure(scrollregion=self.list_canvas.bbox("all"), yscrollincrement=0) # clue_tag = self.clue_direction[0] + str(last_selected_item).zfill(3) self.last_clue_tag = clue_tag self.set_clue_bg(clue_tag, "grey") ##yscrollincrement - increment for vertical scrolling, in pixels, ##millimeters '2m', centimeters '2c', or inches '2i' # list_canvas.yview_scroll(500, "units") #list_canvas.yview_moveto(1) self.list_canvas.bind('<Button-1>', self.on_click)
class ClueListBox(tk.Frame): def __init__(self, parent, clue_direction, clue_list, clue_list_box_height, **kwargs): super().__init__(parent, **kwargs) self.clue_direction = clue_direction self.list_label = tk.Label(self, text=self.clue_direction, fg="black", bg="white", anchor="w") self.list_canvas=tk.Canvas(self, bg='#FFFFFF', width=240, height=clue_list_box_height, scrollregion=(0,0,240,1500)) self.vbar=tk.Scrollbar(self, orient=tk.VERTICAL, bg="white") self.vbar.config(command=self.list_canvas.yview) self.list_canvas.config(yscrollcommand=self.vbar.set) self.list_label.pack(side=tk.TOP, fill=tk.X, expand=1) self.vbar.pack(side=tk.RIGHT,fill=tk.Y) self.list_canvas.pack(side=tk.LEFT,expand=True,fill=tk.BOTH) # These might be constants but they could be configurable in the # future. It seems like that might be a big pain for little benefit. self.bfont = Font(family="Arial", size=10, weight="bold") self.rfont = Font(family="Arial", size=10) self.clue_rect_width = 240 # total width of the clue rectangle self.x_pad = 30 # the amount to pad before starting the clue number self.x_num_pad = 5 # the amount to pad between the clue number and the clue self.y_pad = 5 # the amount to pad (top and bottom) between the end # of the clue box and the bounding box of the clue text # Because our list is fixed in width # the x coords are going to be constant clue_rect_left_x = 0 clue_rect_right_x = self.clue_rect_width # default clue rectangle height: this will change # after we figure out how tall the clue will actually be. self.clue_rect_default_height = 75 clue_rect_top_y = 0 for clue_number, clue_text in clue_list: clue_num_length = self.rfont.measure(clue_number) clue_rect_bottom_y = clue_rect_top_y + self.clue_rect_default_height clue_text_line_len = clue_rect_right_x - ((self.x_pad*2) + clue_num_length + self.x_num_pad) clue_num_left_x = clue_rect_left_x + (self.x_pad - clue_num_length) clue_num_top_y = clue_rect_top_y + self.y_pad clue_text_left_x = clue_num_left_x + clue_num_length + self.x_num_pad clue_text_top_y = clue_rect_top_y + self.y_pad clue_tag = self.clue_direction[0] + str(clue_number).zfill(3) cr = self.list_canvas.create_rectangle(clue_rect_left_x, clue_rect_top_y, clue_rect_right_x, clue_rect_bottom_y, fill="white", tags=clue_tag) cn = self.list_canvas.create_text(clue_num_left_x, clue_num_top_y, text=clue_number, font=self.bfont, anchor = "nw", tags=clue_tag) ct = self.list_canvas.create_text(clue_text_left_x, clue_text_top_y, text=clue_text, font=self.rfont, anchor = "nw", width=clue_text_line_len, tags=clue_tag) bb = self.list_canvas.bbox(ct) x0, y0, x1, y1 = self.list_canvas.coords(cr) y1 = bb[3] + self.y_pad self.list_canvas.coords(cr, x0, y0, x1, y1) clue_rect_top_y = y1 self.list_canvas.itemconfig(cr, outline="white") self.list_canvas.configure(scrollregion = self.list_canvas.bbox("all"), yscrollincrement=0) self.current_clue_tag = self.clue_direction[0] + str(1).zfill(3) self.last_clue_tag = self.current_clue_tag self.set_clue_bg(self.current_clue_tag, "grey") ##yscrollincrement - increment for vertical scrolling, in pixels, ##millimeters '2m', centimeters '2c', or inches '2i' # list_canvas.yview_scroll(500, "units") #list_canvas.yview_moveto(1) self.list_canvas.bind('<Button-1>', self.on_click) def on_click(self, event=None): can_x = self.list_canvas.canvasx(event.x) can_y = self.list_canvas.canvasy(event.y) clicked_item = self.list_canvas.find_closest(can_x, can_y)[0] tags = self.list_canvas.gettags(clicked_item) for tag in tags: if tag[0] == self.clue_direction[0]: self.last_clue_tag = self.current_clue_tag self.current_clue_tag = tag break self.set_clue_bg(self.last_clue_tag, "white") self.set_clue_bg(self.current_clue_tag, "grey") print(self.clue_direction, can_x, can_y, clicked_item, tags) def set_clue_bg(self, clue_tag, clue_bg): clue_canvas_items = self.list_canvas.find_withtag(clue_tag) for item in clue_canvas_items: if self.list_canvas.type(item) == 'rectangle': rect_item = item break self.list_canvas.itemconfig(rect_item, fill=clue_bg)
def __init__(self, dungeon: Dungeon, *args, **kwargs): TextInterface.__init__(self, dungeon) tk.Tk.__init__(self, *args, **kwargs) self.title = "WELCOME TO THE DUNGEON" self.bind('<Key>', self.on_keypress) n, m = dungeon.n, dungeon.m myFont = Font(family="RobotoMono Nerd Font", size=12, weight='normal') myInfoFont = Font(family="RobotoMono Nerd Font", size=10, weight='normal') cell_size = 40 self.fg = fg = '#F2F3F4' bg = '#3B444B' active_fg = '#0095B6' active_bg = '#1B242B' # ───────────────────────── create the grid ────────────────────────── # self.board = tk.Frame(self) self.board.pack(padx=1, pady=1) self.frames = [None for x in range(n * m)] self.cells = [None for x in range(n * m)] for x in range(n * m): r, c = x // m, x % m # ----------- create frames for a fixed size in pixels ----------- # self.frames[x] = tk.Frame(self.board, height=cell_size, width=cell_size, bg=bg) self.frames[x].pack_propagate(False) self.frames[x].grid(row=r, column=c, padx=1, pady=1) # ----------- create the labels to display game state ------------ # self.cells[x] = tk.Label(self.frames[x], bg=bg, fg=fg, font=myFont, activeforeground=active_fg, activebackground=active_bg) self.cells[x].pack(expand=True, fill='both') # ────────────────────────── infos display ─────────────────────────── # f_width = myInfoFont.measure('x') w = max(50, int((cell_size * m) / f_width)) # force_width = tk.Frame(self, width=max(450, cell_size * m)) # force_width.pack_propagate(False) # force_width.pack() frame = tk.Frame(self) frame.pack() self.message_actions = tk.Label(frame, font=myInfoFont, width=w, justify='left') self.message_actions.pack(expand=True, fill='x') self.message_infos = tk.Label(frame, font=myInfoFont, width=w, justify='left') self.message_infos.pack(expand=True, fill='x') self.message_caption = tk.Label(frame, font=myInfoFont, width=w, justify='left') self.message_caption.pack(expand=True, fill='x') # ──────────────────────── display everything ──────────────────────── # self.display()
class T_GUI: def __init__(self, root): self.language = { '阿尔巴尼亚语': 'sq', '阿拉伯语': 'ar', '阿姆哈拉语': 'am', '阿塞拜疆语': 'az', '爱尔兰语': 'ga', '爱沙尼亚语': 'et', '巴斯克语': 'eu', '白俄罗斯语': 'be', '保加利亚语': 'bg', '冰岛语': 'is', '波兰语': 'pl', '波斯尼亚语': 'bs', '波斯语': 'fa', '布尔语南非荷兰语': 'af', '丹麦语': 'da', '德语': 'de', '俄语': 'ru', '法语': 'fr', '菲律宾语': 'tl', '弗里西语': 'fy', '高棉语': 'km', '格鲁吉亚语': 'ka', '古吉拉特语': 'gu', '哈萨克语': 'kk', '海地克里奥尔语': 'ht', '韩语': 'ko', '豪萨语': 'ha', '荷兰语': 'nl', '吉尔吉斯语': 'ky', '加利西亚语': 'gl', '加泰罗尼亚语': 'ca', '捷克语': 'cs', '卡纳达语': 'kn', '科西嘉语': 'co', '克罗地亚语': 'hr', '库尔德语': 'ku', '拉丁语': 'la', '拉脱维亚语': 'lv', '老挝语': 'lo', '立陶宛语': 'lt', '卢森堡语': 'lb', '罗马尼亚语': 'ro', '马尔加什语': 'mg', '马耳他语': 'mt', '马拉地语': 'mr', '马拉雅拉姆语': 'ml', '马来语': 'ms', '马其顿语': 'mk', '毛利语': 'mi', '蒙古语': 'mn', '孟加拉语': 'bn', '缅甸语': 'my', '苗语': 'hmn', '南非科萨语': 'xh', '南非祖鲁语': 'zu', '尼泊尔语': 'ne', '挪威语': 'no', '旁遮普语': 'pa', '葡萄牙语': 'pt', '普什图语': 'ps', '齐切瓦语': 'ny', '日语': 'ja', '瑞典语': 'sv', '萨摩亚语': 'sm', '塞尔维亚语': 'sr', '塞索托语': 'st', '僧伽罗语': 'si', '世界语': 'eo', '斯洛伐克语': 'sk', '斯洛文尼亚语': 'sl', '斯瓦希里语': 'sw', '苏格兰盖尔语': 'gd', '宿务语': 'ceb', '索马里语': 'so', '塔吉克语': 'tg', '泰卢固语': 'te', '泰米尔语': 'ta', '泰语': 'th', '土耳其语': 'tr', '威尔士语': 'cy', '乌尔都语': 'ur', '乌克兰语': 'uk', '乌兹别克语': 'uz', '希伯来语': 'iw', '希腊语': 'el', '西班牙语': 'es', '夏威夷语': 'haw', '信德语': 'sd', '匈牙利语': 'hu', '修纳语': 'sn', '亚美尼亚语': 'hy', '伊博语': 'ig', '意大利语': 'it', '意第绪语': 'yi', '印地语': 'hi', '印尼巽他语': 'su', '印尼语': 'id', '印尼爪哇语': 'jw', '英语': 'en', '约鲁巴语': 'yo', '越南语': 'vi', '中文繁体': 'zh-TW', '中文简体': 'zh-CN' } self.common_list = [['英语'], ['西班牙语'], ['中文简体'], ['日语']] lan_sort_1 = sorted( self.language.keys(), key=lambda char: lazy_pinyin(char)[0][0]) # 排好序的语种list table_len, lan_sort = len(self.language), [] # 按扭最大宽度,表格横竖比 button_max_width, table = len(max(lan_sort_1, key=len)) + 2, (ceil( table_len / 7), 7) for i in range(0, len(lan_sort_1), table[0]): # 格式化成二维数组, lan_sort.append(lan_sort_1[i:i + table[0]]) # 由于后面会进行行纵交换操作,这里取最大值为列数 # print(button_max_width) var, var_1 = StringVar(), StringVar() var.set('自动检测') var_1.set(lan_sort_1[-1]) self.root = root self.root.title('书香年华的专用翻译小工具') self.font = Font() self.frame = ttk.Frame(self.root) self.text = Text(self.frame, width=20, height=10) self.text_out = Text(self.frame) self.button_1 = ttk.Button(self.frame, text='自动检测') self.button_2 = ttk.Button(self.frame, text='<=>') self.button_3 = ttk.Button(self.frame, text='中文简体') self.button_4 = ttk.Button(self.frame, text='翻译') self.top_1 = Toplevel(self.button_1) self.top_2 = Toplevel(self.button_3) self.foc_button = None self.top_3 = Toplevel(self.button_1) self.tree_1 = ttk.Treeview(self.top_1) self.tree_2 = ttk.Treeview(self.top_2) self.tree_3 = ttk.Treeview(self.top_3) self.canvas_1 = Canvas(self.tree_1) self.canvas_2 = Canvas(self.tree_2) self.canvas_3 = Canvas(self.tree_3) self.canvas_1_text = self.canvas_1.create_text(0, 0) self.canvas_2_text = self.canvas_2.create_text(0, 0) self.canvas_3_text = self.canvas_3.create_text(0, 0) self.sizegrip = ttk.Sizegrip(self.frame) # 方便调整窗口大小的部件 # 菜单按钮没写,已完成2018.07.30 # 位置布局 self.frame.grid(row=0, column=0, columnspan=4, sticky=(N, S, E, W)) self.text_out.grid(row=0, column=0, columnspan=4, sticky=(N, S, E, W)) self.button_1.grid(row=1, column=0, sticky=E) self.button_2.grid(row=1, column=1) self.button_3.grid(row=1, column=2, sticky=W) self.button_4.grid(row=1, column=3) self.text.grid(row=2, column=0, columnspan=4, sticky=(N, S, E, W)) self.top_1.grid() self.top_2.grid() self.top_3.grid() self.tree_1.grid() self.tree_2.grid() self.tree_3.grid() self.sizegrip.grid(row=2, column=3, sticky=(S, E)) self.root.rowconfigure(0, weight=1) self.root.columnconfigure(0, weight=1) self.frame.columnconfigure(0, weight=2) self.frame.columnconfigure(1, weight=1) self.frame.columnconfigure(2, weight=1) self.frame.columnconfigure(3, weight=4) self.frame.rowconfigure(0, weight=3) self.frame.rowconfigure(2, weight=1) # 字体设置 self.text_out.tag_configure('font', foreground='#476042', font=('Tempus Sans ITC', 35, 'bold')) self.text_out.tag_configure('font_normal', font=20) self.text_out.tag_configure('font_error', foreground='red', font=('Segoe Print', 16, 'bold')) self.text.insert(END, '输入框', 'SUNKEN') self.text.focus_set() self.text.tag_add('sel', 1.0, END) # 全选文本 # self.frame.tk_focusFollowsMouse() self.text_out.insert(END, '输出框', ('GROOVE', 'font_normal')) self.all_var = [self.text.get(0.0, END), var, var_1, True] self.text_out.configure(state=DISABLED) # 忽略并隐藏窗口 self.top_1.wm_overrideredirect(True) self.top_2.wm_overrideredirect(True) self.top_3.wm_overrideredirect(True) self.top_1.withdraw() self.top_2.withdraw() self.top_3.withdraw() # 事件0 self.text_out.bind('<Button-3>', self._right_click) # 右键复制文本 self.button_1.bind('<Enter>', self._button_menu) self.button_1.bind('<Leave>', self._button_leave) self.button_3.bind('<Enter>', self._button_menu) self.button_3.bind('<Leave>', self._button_leave) # self.top_3.bind('Leave', lambda e: self.top_3.withdraw()) self.top_1.bind('<FocusOut>', self._lose_focus) # 失去焦点隐藏窗口 self.top_2.bind('<FocusOut>', self._lose_focus) self.top_3.bind('<FocusOut>', self._lose_focus) # 在同一个位置,第一次按是tree部件,第二次是canvas部件 self.canvas_1.bind('<ButtonPress-1>', lambda evt: self.top_1.withdraw()) # 画布按下时,隐藏窗口 self.tree_1.bind( '<Configure>', lambda evt: self.canvas_1.grid_forget()) # place_forget()取消选中状态 self.tree_1.bind('<ButtonPress-1>', self._tree_pressed) self.tree_1.bind('<Leave>', lambda e: self.top_1.withdraw()) # 离开某组件,隐藏窗口 self.canvas_2.bind('<ButtonPress-1>', lambda evt: self.top_2.withdraw()) # 画布按下时,隐藏窗口 self.tree_2.bind( '<Configure>', lambda evt: self.canvas_2.grid_forget()) # place_forget()取消选中状态 self.tree_2.bind('<ButtonPress-1>', self._tree_pressed) self.tree_2.bind('<Leave>', lambda e: self.top_2.withdraw()) # 离开某组件,隐藏窗口 self.canvas_3.bind('<ButtonPress-1>', lambda evt: self.top_3.withdraw()) self.tree_3.bind('<Configure>', lambda evt: self.canvas_3.grid_forget()) self.tree_3.bind('<ButtonPress-1>', self._tree_pressed) self.tree_3.bind('<Leave>', lambda e: self.top_3.withdraw()) # 表格设置 # print(table, lan_sort) # 7列 15行 self.tree_1.configure(show='', selectmode='none', height=table[0]) self.tree_2.configure(show='', selectmode='none', height=table[0]) self.tree_3.configure(show='', selectmode='none', height=4) self.tree_1['columns'] = tuple([lan_sort[i] for i in range(table[1])]) # 7行,循环7次 self.tree_2['columns'] = tuple([lan_sort[i] for i in range(table[1])]) self.tree_3['columns'] = ('', ) # 列数 self.items_1 = [ self.tree_1.insert('', 'end', values='') for _ in range(table[0]) ] self.items_2 = [ self.tree_2.insert('', 'end', values='') for _ in range(table[0]) ] self.items_3 = [ self.tree_3.insert('', 'end', values='') for _ in range(4) ] for i in range(1 + 1): # 由于后面会进行行列交换,所以这里添的是列数 self.tree_3.column('#%d' % i, width=button_max_width * 10 + 4) for i in range( table[1] + 1): # 列头不像行头(不纳入计数),默认保留列头(#0),所以得加一,才能显示正确,不然最后一列无法配置width self.tree_1.column('#%d' % i, width=button_max_width * 10 + 4) self.tree_2.column('#%d' % i, width=button_max_width * 10 + 4) if lan_sort[-2] != lan_sort[-1]: # 填补空白数据,使表格在后续处理中不会丢失数据 lan_sort[-1].extend([' '] * (len(lan_sort[-2]) - len(lan_sort[-1]))) table_list = tuple(map(list, zip(*lan_sort))) # 行列交换 # common_table_list = tuple(map(list, zip(*self.common_list))) # print(self.items_2, self.items_3) # table_list = [[row[i] for row in lan_sort] for i in range(len(lan_sort[0]))] for index, item in enumerate(self.items_3): self.tree_3.item(item, values=self.common_list[index]) # print(self.common_list[index]) for index, (item_1, item_2) in enumerate(zip(self.items_1, self.items_2)): self.tree_1.item(item_1, values=table_list[index]) self.tree_2.item(item_2, values=table_list[index]) # print(item_2, table_list[index]) table_list[-2][-1] = '自动检测' # 补充数据 self.tree_1.item('I00E', values=table_list[-2]) self.button_1.config(width=(button_max_width - 3) * 2) self.button_3.config(width=(button_max_width - 3) * 2) self.button_1.configure(command=self._button_menu_1) self.button_3.configure(command=self._button_menu_2) self.button_2.configure(command=self._swap_key) self.button_4.configure(command=self._button_translation) # self.root.attributes('-alpha') # self.root.attributes('-alpha', 0.8) # 设置窗口属性,这里是设置透明度 # 队列 self.q = queue.LifoQueue(9) # self.q.put(list([proxy_main.run, None])) # Thread_0(self.q) # 这里开线程运行附加程序 # list_script = (startup_script, '', self._text_1_show) # self.q.put(list_script) # Thread_0(self.q) self._real_translation() # def _thread_update(self, e): # 创建线程 # Thread_0(e) def _real_update(self, src_text): # 加入数据到队列 fun_list = (self._translator_word, src_text, self._text_1_show) self.q.put(fun_list) # print(threading.activeCount()) 获取线程数 MyTread_0(self.q) def _swap_key(self): # 交换语种 a_var, b_var = self.all_var[1].get(), self.all_var[2].get() if a_var == '自动检测': a_var = '中文简体' if a_var == b_var: return a_var, b_var = b_var, a_var # 这里添加判断语种,根据情况是否put数据到队列 self.button_1.config(text=a_var) self.button_3.config(text=b_var) self._real_update(self.text.get(0.0, END)) sleep(0.1) def _real_translation(self): """实时翻译""" src_text = self.text.get(0.0, END) if not self._is_change(src_text): # print('产生变化') self._real_update(src_text) # print('dd%s无变化' % src_text) self.root.after(2500, self._real_translation) def _right_click(self, _): """右键事件,可能会添加激活控件再操作,已添加聚焦""" self.text_out.focus_set() text_txt = self.text_out.get(0.0, END) copy(text_txt) def _translator_word(self, string_var): # 翻译 """翻译""" # print('数据来了') if self._is_change(string_var) or string_var.strip( ) == '': # 交换语种后与显示语种相同直接返回,字符为空也返回 return # print('有谁', len(string_var.strip()), bool(string_var.strip() == '')) # return self.all_var[1].set(self.button_1['text']) self.all_var[2].set(self.button_3['text']) self.all_var[0] = string_var self._text_1_show(' \n IN translation...\n', 3.5, 3.5, ('GROOVE', 'font')) des_language, src_language = self.all_var[2].get( ), self.all_var[1].get() src_1 = 'auto' if src_language == '自动检测' else self.language[ src_language] # try: # print(string_var, '开始翻译了') des_string = google_tran(string_var, des_1=self.language[des_language], src_1=src_1).text self._text_1_show(des_string, style=('GROOVE', 'font_normal')) # except Exception as e: # 异常处理 # self._text_1_show(e) def _is_change(self, src_text): """判断文本是否变化,判断语种是否改变, 无变化则返回真""" if src_text.strip() == self.all_var[0].strip() and self.all_var[1].get( ) == self.button_1['text']: if self.all_var[2].get() == self.button_3['text']: return True else: return False def _button_translation(self): """翻译按钮""" sleep(0.1) src_text = self.text.get(0.0, END) # print(src_text) if not self._is_change(src_text): self._real_update(src_text) # self.all_var[0] = src_text def _button_menu(self, event): button = event.widget sleep(0.6) in_button = button.winfo_pointerxy() sleep(0.2) out_button = button.winfo_pointerxy() if in_button != out_button: # 在button中停留太短时间不展示菜单 return self.foc_button = button menu_3_geometry = button.winfo_geometry() # print(menu_3_geometry, '在里面') if self.top_1.state() == 'withdrawn' and self.top_2.state( ) == 'withdrawn': x3, y3 = self._position(menu_3_geometry, self.top_3) self._set_top_windows(self.top_3, x3, y3) # sleep(2000) # self.top_3.withdraw() # print('正常', self.top_1.state(), self.top_2.state()) def _button_menu_1(self): """按键菜单""" self.foc_button = self.button_1 menu_1_geometry = self.button_1.winfo_geometry() x1, y1 = self._position(menu_1_geometry) self._set_top_windows(self.top_1, x1, y1) # root x + button x # print(self.top_1.winfo_geometry()) def _button_menu_2(self): self.foc_button = self.button_3 menu_2_geometry = self.button_3.winfo_geometry() x1, y1 = self._position(menu_2_geometry) self._set_top_windows(self.top_2, x1, y1) # print(x1, y1) # root x + button x def _set_top_windows(self, top, x1, y1): # 设置窗口状态 if top is self.top_1: self.button_1.state(['!pressed']) elif top is self.top_2: self.button_3.state(['!pressed']) top.geometry('+%i+%i' % (x1, y1)) top.focus_force() top.deiconify() def _text_1_show(self, snap_string, x=0.0, y=END, style=tuple('GROOVE')): # 刷新输出框 self.text_out.configure(state=NORMAL) self.text_out.delete(x, y) self.text_out.grid(row=0, column=0, columnspan=4) self.text_out.insert(y, snap_string, style) self.text_out.configure(state=DISABLED) def _set_font(self, snap_string=' \n IN translation...\n'): # 字体设置 self._text_1_show(snap_string, x=3.5, y=3.5, style=('GROOVE', 'font')) def _lose_focus(self, event): # 表格失去焦点 if event.widget == self.top_1: self.top_1.withdraw() self.tree_1.state(['!pressed']) else: self.top_2.withdraw() self.tree_2.state(['!pressed']) def _position(self, widget_geometry, top_3=None): """计算语种表格位置""" menu_geometry = widget_geometry # 按扭geometry root_geometry, top_geometry = self.root.winfo_geometry( ), self.top_1.winfo_geometry() menu_position = [int(i) for i in split('[+x]', menu_geometry)] root_position, top_position = [int(i) for i in split('[+x]', root_geometry)], \ [int(i) for i in split('[+x]', top_geometry)] child_window_x = root_position[2] + menu_position[2] + 9 child_window_y = root_position[3] + menu_position[3] - top_position[ 1] + 31 if top_3: top_3_geometry = top_3.winfo_geometry() top_position = [int(i) for i in split('[+x]', top_3_geometry)] # print(top_position) child_window_y = root_position[3] + menu_position[ 3] - top_position[1] + 31 return child_window_x, child_window_y return child_window_x, child_window_y def _button_leave(self, e): x, y, widget_geometry = e.x, e.y, e.widget.winfo_geometry() widget_geometry = [int(i) for i in split('[+x]', widget_geometry)] if not 0 < x < widget_geometry[1] and not y < 0: if self.top_3.state() == 'withdraw': sleep(1) self.top_3.withdraw() # print(x, y, widget_geometry) def _tree_pressed(self, event): # self.top_3.withdraw() x, y, widget = event.x, event.y, event.widget item = widget.identify('item', 0, y) column = widget.identify('column', x, 0) tree_widget = widget.winfo_toplevel() item_values = widget.item(item)['values'] tree_widget.withdraw() if not len(item_values): # 点击到空白表格(行),返回 return text = item_values[int(column[1]) - 1] # 默认tkinter保留列头(#0),所以列数得减一 if not text: # 数据为空,返回 return # button_widget = tree_widget.winfo_toplevel() # print(tree_widget, item_values, text) self.foc_button.config(text=text) bbox = widget.bbox(item, column) self._reveal_selection(text, bbox, widget) def _reveal_selection(self, text, bbox, tree): x, y, width, height = bbox # print(bbox) if tree == self.tree_1: canvas_ = self.canvas_1 canvas_text = self.canvas_1_text elif tree == self.tree_2: canvas_ = self.canvas_2 canvas_text = self.canvas_2_text else: canvas_ = self.canvas_3 canvas_text = self.canvas_3_text self.font.measure(text) canvas_['background'] = 'SystemHighlight' canvas_.configure(width=width, height=height) canvas_.coords(canvas_text, width / 2 - 1, height / 2 - 1) # 居中显示 canvas_.itemconfigure(canvas_text, text=text) canvas_.place(in_=tree, x=x, y=y)
class CodeTree(Treeview): def __init__(self, master): Treeview.__init__(self, master, show='tree', selectmode='none', style='flat.Treeview', padding=4) self.font = Font(self, font="TkDefaultFont 9") self.tag_configure('class', image='img_c') self.tag_configure('def', image='img_f') self.tag_configure('_def', image="img_hf") self.tag_configure('#', image='img_sep') self.tag_configure('cell', image='img_cell') self.callback = None self.cells = [] self.bind('<1>', self._on_click) self.bind('<<TreeviewSelect>>', self._on_select) def _on_click(self, event): if 'indicator' not in self.identify_element(event.x, event.y): self.selection_remove(*self.selection()) self.selection_set(self.identify_row(event.y)) def set_callback(self, fct): self.callback = fct def _on_select(self, event): sel = self.selection() if self.callback is not None and sel: self.callback(*self.item(sel[0], 'values')) def _get_opened(self): opened = [] def rec(item): if self.item(item, 'open'): opened.append(self.item(item, 'tags')) for child in self.get_children(item): rec(child) for item in self.get_children(): rec(item) return opened def populate(self, text, reset): if reset: opened = [] else: opened = self._get_opened() self.delete(*self.get_children()) tokens = tokenize.tokenize(BytesIO(text.encode()).readline) names = set() self.cells.clear() max_length = 20 tree = Tree('', [], -1) tree_index = 0 while True: try: token = tokens.send(None) except StopIteration: break except (tokenize.TokenError, IndentationError): continue add = False if token.type == tokenize.NAME and token.string in ['class', 'def']: obj_type = token.string indent = token.start[1] + 4 token = tokens.send(None) name = token.string names.add(name) if name[0] == '_' and obj_type == 'def': obj_type = '_def' add = True elif token.type == tokenize.COMMENT: if token.string[:5] == '# ---' or 'TODO' in token.string: obj_type = '#' indent = token.start[1] + 4 name = token.string[1:] add = True else: match = re.match(r'^# In(\[.*\].*)$', token.string) if match: obj_type = 'cell' indent = 0 name = match.groups()[0].strip() add = True self.cells.append(token.start[0]) else: match = re.match(r'^# ?%% ?(.*)$', token.string) if match: obj_type = 'cell' indent = 0 name = match.groups()[0].strip() add = True self.cells.append(token.start[0]) if add: tree_index += 1 parent = tree.insert('I-%i' % tree_index, indent) max_length = max(max_length, self.font.measure(name) + 20 + (indent//4 + 1) * 20) tags = (obj_type, name) self.insert(parent, 'end', f'I-{tree_index}', text=name, tags=tags, open=tags in opened, values=('%i.%i' % token.start, '%i.%i' % token.end)) self.column('#0', width=max_length, minwidth=max_length) for item in self.get_children(''): self.item(item, open=True) return names