8
\$\begingroup\$

I wrote a note taking program that works fine however I wanted to converted the program into a more OOP style program. As I am learning the concept of OOP and how classes work I did manage to move my program into a class but it just feels wrong.

I cant be using self. correctly here. I mean it everywhere in the code. The program does work but I just don't think i need almost every line to have self..

I would appreciate a review of the following code as I am still not 100% sure how to build a class properly.

import tkinter as tk
from string import ascii_letters, digits
import tkinter.simpledialog
import tkinter.messagebox
from MINT.myScrollBar import MyScrollbar #customer scroll bar
import psutil
import json
import os
from os import listdir

class MintApp(tk.Frame):

    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, root, *args, **kwargs)
        self.root = root
        self.py_bg_color = "#{:02x}{:02x}{:02x}".format(0,34,64)
        self.py_frame_color = "#{:02x}{:02x}{:02x}".format(0, 23, 45)
        self.root.title("MINT: Mobile Information & Note-taking Tool")
        self.root.config(bg = self.py_frame_color)
        self.root.columnconfigure(0, weight = 0)
        self.root.columnconfigure(1, weight = 1)
        self.root.rowconfigure(0, weight = 0)
        self.root.rowconfigure(1, weight = 1)
        #~~~~~~~~~~~~~~~~~~~< Windows stuff >~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        self.row0label = tk.Label(self.root)
        self.row0label.grid(row = 0, column = 0)
        self.row0label.configure(bg = self.py_frame_color, text = "                                                                              ")
        #~~~~~~~~~~~~~~~~~~~< Variables Defaults >~~~~~~~~~~~~~~~~~~~~~~~
        self.path = "./NotesKeys/"
        self.color_path = "./Colors/"
        self.notebook = dict()
        self.current_working_lib = ""
        self.current_working_keys = ""
        self.list_of_all_filenames = []
        self.current_working_button_color = "orange"
        self.selected_text_color = "orange"
        self.selected_bg_color = "#%02x%02x%02x"
        self.post_update = False
        self.text_wordwrap = False
        self.valid_filename = ""
        self.current_keyword = ""
        #~~~~~~~~~~~~~~~~~~~< USE TO open all files in Directory >~~~~~~

        with open("{}{}".format(self.path, "list_of_all_filenames"), "r") as listall:
            self.list_of_all_filenames = json.load(listall)



        self.openAllFilesInPath(self.path)
        self.base_bg_image = tk.PhotoImage(file="./Colors/pybgbase.png")
        self.bgLable = tk.Label(self.root, image= self.base_bg_image)
        self.bgLable.place(x = 0, y = 0)

        self.bgLable.config(image = self.base_bg_image)
        self.bgLable.image = self.base_bg_image
        self.current_text_color = 'orange'

        self.textFrame = tk.Frame(root, borderwidth = 0, highlightthickness = 0)
        self.textFrame.grid(row = 0, column = 1, columnspan = 1, rowspan = 2, padx = 0, pady = 0, sticky = 'nsew')
        self.textFrame.columnconfigure(0, weight = 1)
        self.textFrame.rowconfigure(0, weight = 1)
        self.textFrame.columnconfigure(1, weight = 0)
        self.textFrame.rowconfigure(1, weight = 0)

        self.entryFrame = tk.Frame(self.root)
        self.entryFrame.grid(row = 0, column = 0, rowspan = 1, columnspan = 1, padx = 0, pady = 0, sticky = 'nsew')
        self.entryFrame.columnconfigure(0, weight = 0)
        self.entryFrame.columnconfigure(1, weight = 0)
        self.entryFrame.rowconfigure(0, weight = 0)
        self.entryFrame.rowconfigure(1, weight = 0)
        self.entryFrame.rowconfigure(2, weight = 0)
        self.entrybg_image = tk.Label(self.entryFrame, image = self.base_bg_image, borderwidth = 0, highlightthickness = 0)
        self.entrybg_image.image = self.base_bg_image
        self.entrybg_image.place(x = 0, y = 0)
        self.entrybg_image.config(image = self.base_bg_image)

        self.kwListFrame = tk.Frame(self.root, borderwidth = 0, highlightthickness = 0)
        self.kwListFrame.grid(row = 1, column = 0, rowspan = 1, columnspan = 1, padx = 0, pady = 0, sticky = 'nsew')
        self.kwListFrame.columnconfigure(0, weight = 1)
        self.kwbg_image = tk.Label(self.kwListFrame, image= self.base_bg_image, borderwidth = 0, highlightthickness = 0)
        self.kwbg_image.image = self.base_bg_image
        self.kwbg_image.place(x = 0, y = 0)
        self.kwbg_image.config(image = self.base_bg_image)

        self.root.textSideL = tk.Text(self.kwListFrame, width = 10, height = 20)
        self.root.textSideL.place( x = 5, y = 5)
        self.root.textSideL.config(wrap = 'none')
        self.root.textSideR = tk.Text(self.kwListFrame,  width = 10, height = 20)
        self.root.textSideR.place( x = 95, y = 5)
        self.root.textSideR.config(wrap = 'none')

        self.statusFrame = tk.Frame(root)
        self.statusFrame.config(bg = self.py_frame_color)
        self.statusFrame.grid(row = 3, column = 0, rowspan = 3, columnspan = 2, padx =0, pady =0, sticky = 'nsew')
        self.statusFrame.columnconfigure(0, weight = 1)
        self.statusFrame.columnconfigure(1, weight = 1)
        self.statusFrame.rowconfigure(0, weight = 0)

        self.root.text = tk.Text(self.textFrame, undo = True)
        self.root.text.grid(row = 0, column = 0, rowspan = 1, columnspan = 1, padx = 0, pady = 0, sticky = 'nsew')
        self.root.text.config(bg = self.py_frame_color, fg = "white", font = ('times', 16), insertbackground = "orange")
        self.root.text.config(wrap = 'none')
        #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        self.statusW = tk.Label(self.statusFrame, font=("times", 16, "bold"), fg = "white", bg = "black", relief = 'sunken', anchor = 'w')
        self.statusW.grid(row = 0, column = 0, padx = 1, pady = 1, sticky = 'sw')
        self.statusW.config(text = "Operation Status", bg = "#%02x%02x%02x" % (0, 23, 45))

        self.statusE = tk.Label(self.statusFrame, font=("times", 16, "bold"), fg = "white", bg = "black", relief = 'sunken', anchor = 'e')
        self.statusE.grid(row = 0, column = 1, padx = 1, pady = 1, sticky = 'se')
        self.statusE.config(bg = "#%02x%02x%02x" % (0, 23, 45))
        #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        self.searchLabel = tk.Label(self.entryFrame)
        self.searchLabel.grid(row = 1, column = 0, padx = 5, pady = 5)
        self.searchLabel.config(text = "Search Text Field")

        self.searchEntry = tk.Entry(self.entryFrame, width = 20)
        self.searchEntry.bind("<Return>", self.searchTextbox)
        self.searchEntry.bind("<Shift-Return>", self.next_match)
        self.searchEntry.bind("<Control-Return>", self.prev_match)
        self.searchEntry.grid(row = 1, column = 1, padx = 5, pady = 5)

        self.keywordLabel = tk.Label(self.entryFrame)
        self.keywordLabel.grid(row = 0, column = 0, padx = 5, pady = 5)
        self.keywordLabel.config(text = "Keyword Search")

        self.keywordEntry = tk.Entry(self.entryFrame, width = 20)
        self.keywordEntry.bind("<Return>", self.kw_entry)
        self.keywordEntry.grid(row = 0, column = 1, padx = 5, pady = 5)

        self.UpdateKeywordsButton = tkinter.Button(self.entryFrame, fg = 'Black', bg = 'Orange', text = "Update Notes", command = self.appendNotes)
        self.UpdateKeywordsButton.grid(row = 2, column = 0, padx = 5, pady = 5)


        self.libraryMenu()
        self.MintThemeDefault("#%02x%02x%02x" % (64,89,82), "#%02x%02x%02x" % (0, 23, 45), "#%02x%02x%02x" % (175, 167, 157), tk.PhotoImage(file = "./Colors/pybgbase.png"))


    def openAllFilesInPath(self, path):
        for filename in listdir(path):
            with open("{}{}".format(path, filename), "r") as f:
                self.notebook[filename] = json.load(f)

    def new_lib_prompt(self):
        a_name = tk.simpledialog.askstring("Create New Note Library", "Alphanumeric and '_' only", initialvalue = "Name_Here")
        valid_chars = "-_.() {}{}".format(ascii_letters, digits)
        self.valid_filename = ("".join(c for c in a_name if c in valid_chars)).replace(" ", "_").lower()
        if self.valid_filename != "" and self.valid_filename != "name_here":
            if self.valid_filename not in self.list_of_all_filenames:
                self.createNewNotesAndKeys(self.valid_filename)
                self.list_of_all_filenames.append(self.valid_filename)
                with open("%s%s"%(self.path, "list_of_all_filenames"), "r+" ) as f:
                        json.dump(self.list_of_all_filenames, f, indent = "")
                self.libraryMenu()
            else:
                print ("Library already exist")
        else:
            print ("No Name Given")

    def createNewNotesAndKeys(self, name):
        n_name = name+"_notes"
        k_name = name+"_keys"
        with open("./NotesKeys/default_notes", "r") as n:
            n_base = json.load(n)
        with open("./NotesKeys/default_keys", "r") as k:
            k_base = json.load(k)
        with open("%s%s" % (self.path,n_name), "w") as new_n:
            json.dump(n_base, new_n, indent = "")
        with open("%s%s" % (self.path,k_name), "w") as new_k:
            json.dump(k_base, new_k, indent = "")
        self.openAllFilesInPath(self.path, self.notebook, "list_of_all_filenames")
    #~~~~~~~~~~~~~~~~~~~< UPDATE keyword display >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def update_kw_display(self):
        list_to_pass = ["chose a library"," chose a library_keys", "chose a library_notes", ""]
        if self.current_working_keys not in list_to_pass:
            keys_to_be_updated = self.notebook[self.current_working_keys]
            self.root.textSideL.delete(1.0, "end-1c")
            self.root.textSideR.delete(1.0, "end-1c")
            contr = 0
            for item in keys_to_be_updated:
                if contr == 0:
                    self.root.textSideL.insert("end-1c",item + "\n")
                    self.root.textSideL.see("end-1c")
                    contr += 1
                else:
                    self.root.textSideR.insert("end-1c",item + "\n")
                    self.root.textSideR.see("end-1c")
                    contr = 0
        else:
            print("In the list to pass")
    #~~~~~~~~~~~~~~~~~~~< Search for words and highlight >~~~~~~~~~~~~~~~~~~~~~~~~
    def searchTextbox(self, event = None):
        self.root.text.tag_delete("search")
        self.root.text.tag_configure("search", background="green")
        start = "1.0"
        if len(self.searchEntry.get()) > 0:
            self.root.text.mark_set("insert", self.root.text.search(self.searchEntry.get(), start, nocase = None))
            self.root.text.see("insert")
            while True:
                pos = self.root.text.search(self.searchEntry.get(), start, 'end', nocase = None) 
                if pos == "": 
                    break       
                start = pos + "+%dc" % len(self.searchEntry.get()) 
                self.root.text.tag_add("search", pos, "%s + %dc" % (pos, len(self.searchEntry.get())))
        else:
            pass

    def next_match(self, event = None):
        # move cursor to end of current match
        while (self.root.text.compare("insert", "<", "end") and "search" in self.root.text.tag_names("insert")):
            self.root.text.mark_set("insert", "insert+1c")
        # find next character with the tag
        next_match = self.root.text.tag_nextrange("search", "insert")
        if next_match:
            self.root.text.mark_set("insert", next_match[0])
            self.root.text.see("insert")
        # prevent default behavior, in case this was called
        # via a key binding
        return "break"

    def prev_match(self, event = None):
        # move cursor to end of current match
        while (self.root.text.compare("insert", "<", "end") and "search" in self.root.text.tag_names("insert")):
            self.root.text.mark_set("insert", "insert-1c")
        # find previous character with the tag
        prev_match = self.root.text.tag_prevrange("search", "insert")
        if prev_match:
            self.root.text.mark_set("insert", prev_match[0])
            self.root.text.see("insert")
        # prevent default behavior, in case this was called
        # via a key binding
        return "break"
    #~~~~~~~~~~~~~~~~~~~< UPDATE selected_notes! >~~~~~~~~~~~~~~~~~~~
    def appendNotes(self):
        e1_current = self.keywordEntry.get().lower()
        e1_all_case = self.keywordEntry.get()
        e2_current = self.root.text.get(1.0, "end-1c")
        answer = tkinter.messagebox.askquestion("Update Notes!","Are you sure you want update your Notes for " + e1_all_case + " This cannot be undone!")
        if answer == "yes":
            if e1_current in self.notebook[self.current_working_lib]:
                self.statusE.config(text = "Updating Keyword & Notes for the " + self.current_working_lib + " Library!")
                dict_to_be_updated = self.notebook[self.current_working_lib]
                dict_to_be_updated[e1_current] = e2_current
                with open("%s%s" % (self.path, self.current_working_lib),"w") as working_temp_var:
                    json.dump(dict_to_be_updated, working_temp_var, indent = "")
                self.statusE.config(text = "Update Complete")          
            else:
                self.statusE.config(text= "Creating New Keyword & Notes for the " + self.current_working_lib + " Library!")
                dict_to_be_updated = self.notebook[self.current_working_lib]
                dict_to_be_updated[e1_current] = e2_current
                with open("%s%s" % (self.path, self.current_working_lib), "w" ) as working_temp_var:
                    json.dump(dict_to_be_updated, working_temp_var, indent = "")
                keys_to_be_updated = self.notebook[self.current_working_keys]
                keys_to_be_updated.append(e1_all_case)
                with open("%s%s" % (self.path, self.current_working_keys), "w" ) as working_temp_keys:
                    json.dump(keys_to_be_updated, working_temp_keys, indent = "")
                self.statusE.config(text = "Update Complete")
            self.update_kw_display()            
        else:
            tkinter.messagebox.showinfo("...", "That was close!")      
    #~~~~~~~~~~~~~~~~~~~< Entry Widget >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def kw_entry(self, event = None):
        e1_current = self.keywordEntry.get().lower()
        if self.current_keyword == e1_current:
            print("Already editing current keyword")
            pass
        else:
            answer = tkinter.messagebox.askquestion("Changing Notes!","Are you sure you want change the current Notes section to " + e1_current + "? Any unsaved changed will be lost!")
            if answer == "yes":
                if self.current_working_lib in self.notebook:
                    note_var = self.notebook[self.current_working_lib]
                    if e1_current in note_var:
                        self.root.text.delete(1.0, "end-1c")
                        self.root.text.insert("end-1c", note_var[e1_current])
                        self.root.text.see("end-1c")
                        self.current_keyword = e1_current
                    else:
                        self.root.text.delete(1.0, "end-1c")
                        self.root.text.insert("end-1c", "Not a Keyword")
                        self.root.text.see("end-1c")
                        self.current_keyword = e1_current
                else:
                    self.root.text.delete(1.0, "end-1c")
                    self.root.text.insert("end-1c", "No Library Selected")
                    self.root.text.see("end-1c")
            else:
                pass
    #~~~~~~~~~~~~~~~~~~~< Preset Themes >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    def MintThemeDefault(self, main_bg, text_bg, txt_color, bg_image):
        self.currentTextColor = txt_color
        themebg_image = bg_image
        self.textFrame.config(bg = text_bg)
        self.entrybg_image.config(image = themebg_image)
        self.entrybg_image.image = themebg_image
        self.kwbg_image.config(image = themebg_image)
        self.kwbg_image.image = themebg_image
        self.bgLable.config(image = themebg_image)
        self.bgLable.image = themebg_image
        self.root.config(bg = main_bg)
        self.root.text.config(bg = text_bg, fg = txt_color)
        self.root.textSideL.config(bg = text_bg, fg = txt_color)
        self.root.textSideR.config(bg = text_bg, fg = txt_color)
        self.searchEntry.config(fg = txt_color, bg = text_bg)
        self.keywordEntry.config(fg = txt_color, bg = text_bg)
        self.statusFrame.config(bg = text_bg)
        self.statusE.config(fg = txt_color, bg = text_bg)
        self.statusW.config(fg = txt_color, bg = text_bg)
        self.searchLabel.config(fg = txt_color, bg = text_bg)
        self.keywordLabel.config(fg = txt_color, bg = text_bg)
        self.UpdateKeywordsButton.config(fg = txt_color, bg = text_bg)
    #~~~~~~~~~~~~~~~~~~< Custom Scroll Bar >~~~~~~~~~~~~~~~~~~~
        self.vScrollBar = MyScrollbar(self.textFrame, width = 15, command = root.text.yview, troughcolor = text_bg,
                                                buttontype = 'square', thumbcolor = txt_color, buttoncolor = main_bg)
        self.vScrollBar.grid(row = 0, column = 2, columnspan = 1, rowspan = 1, padx = 0, pady = 0, sticky = 'nse')
        self.root.text.configure(yscrollcommand = self.vScrollBar.set)
        self.vScrollBar.config(background = main_bg)
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        self.hScrollBar = MyScrollbar(self.textFrame, height = 15, command = root.text.xview, orient = 'horizontal', troughcolor = text_bg,
                                                buttontype = 'square', thumbcolor = txt_color, buttoncolor = main_bg)
        self.hScrollBar.grid(row = 1 , column = 0, columnspan = 1, rowspan = 1, padx = 0, pady = 0, sticky = 'sew')
        self.root.text.configure(xscrollcommand = self.hScrollBar.set)
        self.hScrollBar.config(background = main_bg)
    #~~~~~~~~~~~~~~~~~~< Theme Manager >~~~~~~~~~~~~~~~~~~~~~~~~
    def MintTheme1(self):
        main_bg_color = "#%02x%02x%02x" % (64, 89, 82)
        text_bg_color = "#%02x%02x%02x" % (17, 41, 41)
        txt_color = "#%02x%02x%02x" % (175, 167, 157)
        bg_image = tk.PhotoImage(file = "./Colors/theme1bg.png")
        self.MintThemeDefault(main_bg_color, text_bg_color, txt_color, bg_image)
    def MintTheme2(self):
        main_bg_color = "#%02x%02x%02x" % (14, 51, 51)
        text_bg_color = "#%02x%02x%02x" % (4, 22, 22)
        txt_color = "#%02x%02x%02x" % (223, 171, 111)
        bg_image = tk.PhotoImage(file="./Colors/theme2bg.png")
        self.MintThemeDefault(main_bg_color, text_bg_color, txt_color, bg_image)
    #~~~~~~~~~~~~~~~~~~~< Toggle Wordwrap >~~~~~~~~~~~~~~~~~~~~~~~~~~~    
    def toggleWordWrap(self):
        if self.text_wordwrap == False:
            self.root.text.config(wrap = 'char')
            self.text_wordwrap = True
        else:
            self.root.text.config(wrap = 'none')
            self.text_wordwrap = False
    #~~~~~~~~~~~~~~~~~~~< Menu function >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def updateWorkingLibKeys(self, filename):
        self.current_working_lib = "{}_notes".format(filename).lower()
        self.current_working_keys = "{}_keys".format(filename).lower()
        self.update_kw_display()
    def doNothing(self):
        pass
    def libraryMenu(self):
        self.menu = tk.Menu(self.root)
        self.root.config(menu = self.menu)
        self.fileMenu = tk.Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "File", menu = self.fileMenu)
        self.fileMenu.add_command(label = "Save", command = self.doNothing)
        self.fileMenu.add_command(label = "Save As", command = self.doNothing)
        self.fileMenu.add_separator()
        self.fileMenu.add_command(label = "Exit", command = lambda: self.closeProgram(self.root))

        self.libMenu = tk.Menu(self.menu)
        self.menu.add_cascade(label = "Note Libraries", menu = self.libMenu)
        self.libMenu.add_command(label = "Library Help Page - Not Implemented Yet", command = self.doNothing)
        self.libMenu.add_separator()
        self.libMenu.add_command(label = "New Library", command = self.new_lib_prompt)
        self.libMenu.add_command(label = "Lock Library - Not Implemented Yet", command = self.doNothing)
        self.libMenu.add_command(label = "Delete Library! - Not Implemented Yet", command = self.doNothing)
        self.libMenu.add_separator()

        self.prefMenu = tk.Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "Preferences", menu = self.prefMenu)
        self.prefMenu.add_command(label = "Mint Theme 1", command = self.MintTheme1)
        self.prefMenu.add_command(label = "Mint Theme 2", command = self.MintTheme2)
        self.libMenu.add_separator()
        self.prefMenu.add_command(label = "Toggle Word-Wrap", command = self.toggleWordWrap)

        self.helpMenu = tk.Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "Help", menu = self.helpMenu)
        self.helpMenu.add_command(label = "Info", command = self.doNothing)

        for filename in self.list_of_all_filenames:
            self.libMenu.add_command(label = "%s" % (filename), command = lambda filename = filename: self.updateWorkingLibKeys(filename))
    #~~~~~~~~~~~~~~~~~~~< Close >~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def closeProgram(self):
        answer = tkinter.messagebox.askquestion("Leaving MINT?", "Are you sure you want to leave MINT")
        if answer == "yes":
            answer = tkinter.messagebox.askquestion("Save work?", "Would you like to save before you exit MINT?")
            if answer == "yes":
                self.appendNotes
                self.root.destroy()
            else:
                self.root.destroy()
        else:
            tkinter.messagebox.showinfo("MINTy Fresh!", "Welcome Back XD")


if __name__ == "__main__":
    root = tk.Tk()

    MyApp = MintApp(root)
    print(MyApp.notebook["py_keys"])

    root.mainloop()

I don't think it matters but for reference here is the code I started with before I decided to move everything into a class.

from tkinter import Tk, Label, PhotoImage, Menu, Frame, Text, Entry
from MINT.MintPack import doNothing, openAllFilesInPath, militaryTime  # my module
from string import ascii_letters, digits
import tkinter.simpledialog
import tkinter.messagebox
from MINT.myScrollBar import MyScrollbar
import psutil
import json
import os
# Created on Mar 21, 2017
# @author: Michael A McDonnal
# py_bg_color =  "#%02x%02x%02x" % (0, 34, 64)
py_bg_color =  "#%02x%02x%02x".format((0, '02x'), (34, '02x'), (64, '02x'))
py_frame_color =  "#%02x%02x%02x" % (0, 23, 45)
root = Tk()
root.title("MINT:    Mobile Information & Note-taking Tool")
# root.geometry("1050x900")
# root.minsize(800, 600)

root.config(bg = py_frame_color)
root.columnconfigure(0, weight = 0)
root.columnconfigure(1, weight = 1)
root.rowconfigure(0, weight = 0)
root.rowconfigure(1, weight = 1)
#root.rowconfigure(2, weight = 1)
#~~~~~~~~~~~~~~~~~~~< Windows stuff >~~~~~~~~~~~~~~~~~~~~~~~~~~~~
row0label = Label(root)
row0label.grid(row = 0, column = 0)
row0label.configure(text = "                                                                              ")
#~~~~~~~~~~~~~~~~~~~< Variables Defaults >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
path = "./NotesKeys/"
color_path = "./Colors/"
notebook = dict()
current_working_lib = ""
current_working_keys = ""
list_of_all_filenames = []
current_working_button_color = "orange"
selected_text_color = "orange"
selected_bg_color = "#%02x%02x%02x"
post_update = False
text_wordwrap = False
#~~~~~~~~~~~~~~~~~~~< USE TO open all files in Directory >~~~~~~~~~~~~~~~~~~~
with open("{}{}".format(path, "list_of_all_filenames"), "r") as listall:
    list_of_all_filenames = json.load(listall)

openAllFilesInPath(path, notebook)
#~~~~~~~~~~~~~~~~~~~< Prompt For New Library >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
valid_filename = ""
def new_lib_prompt():
    global list_of_all_filenames, path
    a_name = tkinter.simpledialog.askstring("Create New Note Library", "Alphanumeric and '_' only", initialvalue = "Name_Here")
    valid_chars = "-_.() {}{}".format(ascii_letters, digits)
    valid_filename = ("".join(c for c in a_name if c in valid_chars)).replace(" ", "_").lower()
    if valid_filename != "" and valid_filename != "name_here":
        if valid_filename not in list_of_all_filenames:
            createNewNotesAndKeys(valid_filename)
            list_of_all_filenames.append(valid_filename)
            with open("%s%s"%(path, "list_of_all_filenames"), "r+" ) as f:
                    json.dump(list_of_all_filenames, f, indent = "")
            libraryMenu()
        else:
            print ("Library already exist")
    else:
        print ("No Name Given")

def createNewNotesAndKeys(name):
    n_name = name+"_notes"
    k_name = name+"_keys"
    with open("./NotesKeys/default_notes", "r") as n:
        n_base = json.load(n)
    with open("./NotesKeys/default_keys", "r") as k:
        k_base = json.load(k)
    with open("%s%s" % (path,n_name), "w") as new_n:
        json.dump(n_base, new_n, indent = "")
    with open("%s%s" % (path,k_name), "w") as new_k:
        json.dump(k_base, new_k, indent = "")
    openAllFilesInPath(path, notebook, "list_of_all_filenames")
#~~~~~~~~~~~~~~~~~~~< UPDATE keyword display >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def update_kw_display():
    list_to_pass = ["chose a library"," chose a library_keys", "chose a library_notes", ""]
    if current_working_keys not in list_to_pass:
        keys_to_be_updated = notebook[current_working_keys]
        root.textSideL.delete(1.0, "end-1c")
        root.textSideR.delete(1.0, "end-1c")
        contr = 0
        for item in keys_to_be_updated:
            if contr == 0:
                root.textSideL.insert("end-1c",item + "\n")
                root.textSideL.see("end-1c")
                contr += 1
            else:
                root.textSideR.insert("end-1c",item + "\n")
                root.textSideR.see("end-1c")
                contr = 0
    else:
        print("In the list to pass")
#~~~~~~~~~~~~~~~~~~~< Search for words and highlight >~~~~~~~~~~~~~~~~~~~~~~~~
def searchTextbox(event = None):
    root.text.tag_delete("search")
    root.text.tag_configure("search", background="green")
    start = "1.0"
    if len(searchEntry.get()) > 0:
        root.text.mark_set("insert", root.text.search(searchEntry.get(), start, nocase = None))
        root.text.see("insert")
        while True:
            pos = root.text.search(searchEntry.get(), start, 'end', nocase = None) 
            if pos == "": 
                break       
            start = pos + "+%dc" % len(searchEntry.get()) 
            root.text.tag_add("search", pos, "%s + %dc" % (pos, len(searchEntry.get())))
    else:
        pass

def next_match(event = None):
    # move cursor to end of current match
    while (root.text.compare("insert", "<", "end") and "search" in root.text.tag_names("insert")):
        root.text.mark_set("insert", "insert+1c")
    # find next character with the tag
    next_match = root.text.tag_nextrange("search", "insert")
    if next_match:
        root.text.mark_set("insert", next_match[0])
        root.text.see("insert")
    # prevent default behavior, in case this was called
    # via a key binding
    return "break"

def prev_match(event = None):
    # move cursor to end of current match
    while (root.text.compare("insert", "<", "end") and "search" in root.text.tag_names("insert")):
        root.text.mark_set("insert", "insert-1c")
    # find previous character with the tag
    prev_match = root.text.tag_prevrange("search", "insert")
    if prev_match:
        root.text.mark_set("insert", prev_match[0])
        root.text.see("insert")
    # prevent default behavior, in case this was called
    # via a key binding
    return "break"
#~~~~~~~~~~~~~~~~~~~< UPDATE selected_notes! >~~~~~~~~~~~~~~~~~~~
def appendNotes():
    global current_working_lib, current_working_keys, path
    e1_current = keywordEntry.get().lower()
    e1_all_case = keywordEntry.get()
    e2_current = root.text.get(1.0, "end-1c")
    answer = tkinter.messagebox.askquestion("Update Notes!","Are you sure you want update your Notes for " + e1_all_case + " This cannot be undone!")
    if answer == "yes":
        if e1_current in notebook[current_working_lib]:
            statusE.config(text = "Updating Keyword & Notes for the " + current_working_lib + " Library!")
            dict_to_be_updated = notebook[current_working_lib]
            dict_to_be_updated[e1_current] = e2_current
            with open("%s%s" % (path, current_working_lib),"w") as working_temp_var:
                json.dump(dict_to_be_updated, working_temp_var, indent = "")
            statusE.config(text = "Update Complete")          
        else:
            statusE.config(text= "Creating New Keyword & Notes for the " + current_working_lib + " Library!")
            dict_to_be_updated = notebook[current_working_lib]
            dict_to_be_updated[e1_current] = e2_current
            with open("%s%s" % (path, current_working_lib), "w" ) as working_temp_var:
                json.dump(dict_to_be_updated, working_temp_var, indent = "")
            keys_to_be_updated = notebook[current_working_keys]
            keys_to_be_updated.append(e1_all_case)
            with open("%s%s" % (path, current_working_keys), "w" ) as working_temp_keys:
                json.dump(keys_to_be_updated, working_temp_keys, indent = "")
            statusE.config(text = "Update Complete")
        update_kw_display()            
    else:
        tkinter.messagebox.showinfo("...", "That was close!")      
#~~~~~~~~~~~~~~~~~~~< Entry Widget >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
current_keyword = ""
def kw_entry(event = None):
    global current_working_lib, current_keyword
    e1_current = keywordEntry.get().lower()
    if current_keyword == e1_current:
        print("Already editing current keyword")
        pass
    else:
        answer = tkinter.messagebox.askquestion("Changing Notes!","Are you sure you want change the current Notes section to " + e1_current + "? Any unsaved changed will be lost!")
        if answer == "yes":
            if current_working_lib in notebook:
                note_var = notebook[current_working_lib]
                if e1_current in note_var:
                    root.text.delete(1.0, "end-1c")
                    root.text.insert("end-1c", note_var[e1_current])
                    root.text.see("end-1c")
                    current_keyword = e1_current
                else:
                    root.text.delete(1.0, "end-1c")
                    root.text.insert("end-1c", "Not a Keyword")
                    root.text.see("end-1c")
                    current_keyword = e1_current
            else:
                root.text.delete(1.0, "end-1c")
                root.text.insert("end-1c", "No Library Selected")
                root.text.see("end-1c")
        else:
            pass
#~~~~~~~~~~~~~~~~~~~< Preset Themes >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
base_bg_image = PhotoImage(file="./Colors/pybgbase.png")
bgLable = Label(root, image= base_bg_image)
bgLable.place(x = 0, y = 0)

bgLable.config(image = base_bg_image)
bgLable.image = base_bg_image
current_text_color = 'orange'

def MintThemeDefault(main_bg, text_bg, txt_color, bg_image):
    global currentTextColor
    currentTextColor = txt_color
    themebg_image = bg_image
    textFrame.config(bg = text_bg)
    entrybg_image.config(image = themebg_image)
    entrybg_image.image = themebg_image
    kwbg_image.config(image = themebg_image)
    kwbg_image.image = themebg_image
    bgLable.config(image = themebg_image)
    bgLable.image = themebg_image
    root.config(bg = main_bg)
    root.text.config(bg = text_bg, fg = txt_color)
    root.textSideL.config(bg = text_bg, fg = txt_color)
    root.textSideR.config(bg = text_bg, fg = txt_color)
    searchEntry.config(fg = txt_color, bg = text_bg)
    keywordEntry.config(fg = txt_color, bg = text_bg)
    statusFrame.config(bg = text_bg)
    statusE.config(fg = txt_color, bg = text_bg)
    statusW.config(fg = txt_color, bg = text_bg)
    searchLabel.config(fg = txt_color, bg = text_bg)
    keywordLabel.config(fg = txt_color, bg = text_bg)
    UpdateKeywordsButton.config(fg = txt_color, bg = text_bg)
#~~~~~~~~~~~~~~~~~~< Custome Scroll Bar >~~~~~~~~~~~~~~~~~~~
    vScrollBar = MyScrollbar(textFrame, width = 15, command = root.text.yview, troughcolor = text_bg,
                                            buttontype = 'square', thumbcolor = txt_color, buttoncolor = main_bg)
    vScrollBar.grid(row = 0, column = 2, columnspan = 1, rowspan = 1, padx = 0, pady = 0, sticky = 'nse')
    root.text.configure(yscrollcommand = vScrollBar.set)
    vScrollBar.config(background = main_bg)
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    hScrollBar = MyScrollbar(textFrame, height = 15, command = root.text.xview, orient = 'horizontal', troughcolor = text_bg,
                                            buttontype = 'square', thumbcolor = txt_color, buttoncolor = main_bg)
    hScrollBar.grid(row = 1 , column = 0, columnspan = 1, rowspan = 1, padx = 0, pady = 0, sticky = 'sew')
    root.text.configure(xscrollcommand = hScrollBar.set)
    hScrollBar.config(background = main_bg)
#~~~~~~~~~~~~~~~~~~< Theme Manager >~~~~~~~~~~~~~~~~~~~~~~~~
def MintTheme1():
    main_bg_color = "#%02x%02x%02x" % (64, 89, 82)
    text_bg_color = "#%02x%02x%02x" % (17, 41, 41)
    txt_color = "#%02x%02x%02x" % (175, 167, 157)
    bg_image = PhotoImage(file = "./Colors/theme1bg.png")
    MintThemeDefault(main_bg_color, text_bg_color, txt_color, bg_image)
def MintTheme2():
    global currentTextColor
    main_bg_color = "#%02x%02x%02x" % (14, 51, 51)
    text_bg_color = "#%02x%02x%02x" % (4, 22, 22)
    txt_color = "#%02x%02x%02x" % (223, 171, 111)
    bg_image = PhotoImage(file="./Colors/theme2bg.png")
    MintThemeDefault(main_bg_color, text_bg_color, txt_color, bg_image)
#~~~~~~~~~~~~~~~~~~~< Toggle Wordwrap >~~~~~~~~~~~~~~~~~~~~~~~~~~~    
def toggleWordWrap():
    global text_wordwrap
    if text_wordwrap == False:
        root.text.config(wrap = 'char')
        text_wordwrap = True
    else:
        root.text.config(wrap = 'none')
        text_wordwrap = False
#~~~~~~~~~~~~~~~~~~~< Menu function >~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def updateWorkingLibKeys(filename):
    global current_working_lib,current_working_keys
    current_working_lib = "{}_notes".format(filename).lower()
    current_working_keys = "{}_keys".format(filename).lower()
    update_kw_display()
def libraryMenu():
    menu = Menu(root)
    root.config(menu = menu)
    fileMenu = Menu(menu, tearoff = 0)
    menu.add_cascade(label = "File", menu = fileMenu)
    fileMenu.add_command(label = "Save", command = doNothing)
    fileMenu.add_command(label = "Save As", command = doNothing)
    fileMenu.add_separator()
    fileMenu.add_command(label = "Exit", command = lambda: closeProgram(root))

    libMenu = Menu(menu, tearoff = 0)
    menu.add_cascade(label = "Note Libraries", menu = libMenu)
    libMenu.add_command(label = "Library Help Page - Not Implemented Yet", command = doNothing)
    libMenu.add_separator()
    libMenu.add_command(label = "New Library", command = new_lib_prompt)
    libMenu.add_command(label = "Lock Library - Not Implemented Yet", command = doNothing)
    libMenu.add_command(label = "Delete Library! - Not Implemented Yet", command = doNothing)
    libMenu.add_separator()

    prefMenu = Menu(menu, tearoff = 0)
    menu.add_cascade(label = "Preferences", menu = prefMenu)
    prefMenu.add_command(label = "Mint Theme 1", command = MintTheme1)
    prefMenu.add_command(label = "Mint Theme 2", command = MintTheme2)
    libMenu.add_separator()
    prefMenu.add_command(label = "Toggle Word-Wrap", command = toggleWordWrap)

    helpMenu = Menu(menu, tearoff = 0)
    menu.add_cascade(label = "Help", menu = helpMenu)
    helpMenu.add_command(label = "Info", command = doNothing)

    for filename in list_of_all_filenames:
        libMenu.add_command(label = "%s" % (filename), command = lambda filename = filename: updateWorkingLibKeys(filename))

libraryMenu()
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
textFrame = Frame(root, borderwidth = 0, highlightthickness = 0)
textFrame.grid(row = 0, column = 1, columnspan = 1, rowspan = 2, padx = 0, pady = 0, sticky = 'nsew')
textFrame.columnconfigure(0, weight = 1)
textFrame.rowconfigure(0, weight = 1)
textFrame.columnconfigure(1, weight = 0)
textFrame.rowconfigure(1, weight = 0)

entryFrame = Frame(root)
entryFrame.grid(row = 0, column = 0, rowspan = 1, columnspan = 1, padx = 0, pady = 0, sticky = 'nsew')
entryFrame.columnconfigure(0, weight = 0)
entryFrame.columnconfigure(1, weight = 0)
entryFrame.rowconfigure(0, weight = 0)
entryFrame.rowconfigure(1, weight = 0)
entryFrame.rowconfigure(2, weight = 0)
entrybg_image = Label(entryFrame, image = base_bg_image, borderwidth = 0, highlightthickness = 0)
entrybg_image.image = base_bg_image
entrybg_image.place(x = 0, y = 0)
entrybg_image.config(image = base_bg_image)

kwListFrame = Frame(root, borderwidth = 0, highlightthickness = 0)
kwListFrame.grid(row = 1, column = 0, rowspan = 1, columnspan = 1, padx = 0, pady = 0, sticky = 'nsew')
kwListFrame.columnconfigure(0, weight = 1)
kwbg_image = Label(kwListFrame, image= base_bg_image, borderwidth = 0, highlightthickness = 0)
kwbg_image.image = base_bg_image
kwbg_image.place(x = 0, y = 0)
kwbg_image.config(image = base_bg_image)

root.textSideL = Text(kwListFrame, width = 10, height = 20)
root.textSideL.place( x = 5, y = 5)
root.textSideL.config(wrap = 'none')
root.textSideR = Text(kwListFrame,  width = 10, height = 20)
root.textSideR.place( x = 95, y = 5)
root.textSideR.config(wrap = 'none')

statusFrame = Frame(root)
statusFrame.config(bg = py_frame_color)
statusFrame.grid(row = 3, column = 0, rowspan = 3, columnspan = 2, padx =0, pady =0, sticky = 'nsew')
statusFrame.columnconfigure(0, weight = 1)
statusFrame.columnconfigure(1, weight = 1)
statusFrame.rowconfigure(0, weight = 0)

root.text = Text(textFrame, undo = True)
root.text.grid(row = 0, column = 0, rowspan = 1, columnspan = 1, padx = 0, pady = 0, sticky = 'nsew')
root.text.config(bg = py_frame_color, fg = "white", font = ('times', 16), insertbackground = "orange")
root.text.config(wrap = 'none')
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
statusW = Label(statusFrame, font=("times", 16, "bold"), fg = "white", bg = "black", relief = 'sunken', anchor = 'w')
statusW.grid(row = 0, column = 0, padx = 1, pady = 1, sticky = 'sw')
statusW.config(text = "Operation Status", bg = "#%02x%02x%02x" % (0, 23, 45))

statusE = Label(statusFrame, font=("times", 16, "bold"), fg = "white", bg = "black", relief = 'sunken', anchor = 'e')
statusE.grid(row = 0, column = 1, padx = 1, pady = 1, sticky = 'se')
statusE.config(bg = "#%02x%02x%02x" % (0, 23, 45))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
searchLabel = Label(entryFrame)
searchLabel.grid(row = 1, column = 0, padx = 5, pady = 5)
searchLabel.config(text = "Search Text Field")

searchEntry = Entry(entryFrame, width = 20)
searchEntry.bind("<Return>", searchTextbox)
searchEntry.bind("<Shift-Return>", next_match)
searchEntry.bind("<Control-Return>", prev_match)
searchEntry.grid(row = 1, column = 1, padx = 5, pady = 5)

keywordLabel = Label(entryFrame)
keywordLabel.grid(row = 0, column = 0, padx = 5, pady = 5)
keywordLabel.config(text = "Keyword Search")

keywordEntry = Entry(entryFrame, width = 20)
keywordEntry.bind("<Return>", kw_entry)
keywordEntry.grid(row = 0, column = 1, padx = 5, pady = 5)

UpdateKeywordsButton = tkinter.Button(entryFrame, fg = 'Black', bg = 'Orange', text = "Update Notes", command = appendNotes)
UpdateKeywordsButton.grid(row = 2, column = 0, padx = 5, pady = 5)

MintThemeDefault("#%02x%02x%02x" % (64,89,82), "#%02x%02x%02x" % (0, 23, 45), "#%02x%02x%02x" % (175, 167, 157), PhotoImage(file = "./Colors/pybgbase.png"))
#~~~~~~~~~~~~~~~~~~~< Close Program >~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def statusClock():
    statusE.config(text = "{}".format(militaryTime()) + "  Preparing to do nothing...")
    statusE.after(200, statusClock)
statusClock()
#~~~~~~~~~~~~~~~~~~~< Close Program >~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def closeProgram():
    answer = tkinter.messagebox.askquestion("Leaving MINT?", "Are you sure you want to leave MINT")
    if answer == "yes":
        answer = tkinter.messagebox.askquestion("Save work?", "Would you like to save before you exit MINT?")
        if answer == "yes":
            appendNotes
            root.destroy()
        else:
            root.destroy()
    else:
        tkinter.messagebox.showinfo("MINTy Fresh!", "Welcome Back XD")
#~~~~~~~~~~~~~~~~~~~< root Main Loop >~~~~~~~~~~~~~~~~~~~~~~~~~~~~
process = psutil.Process(os.getpid())
print(process.memory_info().rss)
root.protocol("WM_DELETE_WINDOW", closeProgram)
root.mainloop()
\$\endgroup\$
5
  • \$\begingroup\$ are there no implicit receivers in python - if it's like ruby you can drop the self -- but i don't know that for sure \$\endgroup\$
    – BenKoshy
    Jun 9, 2017 at 21:53
  • \$\begingroup\$ are you able to describe in psudo code what is going here - because it's not easy to follow if you haven't written it from a reviewer's point of view. \$\endgroup\$
    – BenKoshy
    Jun 9, 2017 at 21:55
  • \$\begingroup\$ @BKSpurgeon: From what I understand the self is required for many aspects of the class. Python is the only language I have been learning so I don't know about ruby. What do you mean when you say sudo code? As I understand it from previous conversation hear on codereview the reviewers want the complete code as is. \$\endgroup\$
    – Mike - SMT
    Jun 9, 2017 at 22:14
  • \$\begingroup\$ yes sir that is true people want the full code, but i would suggest adding explanations as well because personally i found it hard to follow/understand what was being done and why. when i mean psuedocode i personally mean high level description. here's something random: e.g. 1. user logs in. 2. up to date statistics are shown to reader 3. users automatically logged out after 2 minutes. \$\endgroup\$
    – BenKoshy
    Jun 10, 2017 at 7:01
  • \$\begingroup\$ By the way, the correct spelling is pseudo code. \$\endgroup\$ Jun 10, 2017 at 17:40

2 Answers 2

3
+50
\$\begingroup\$

I am new to programming in Python too and struggled with exactly the same issue about two months ago trying to build app in tkinter :). I will try to explain in terms as I understand the concept and hopefully it will shed some light on to what you are trying to achieve but I am far from expert :).

The idea of using self.variable = value is to add this value as an attribute of the object (self) and then be able to recall that value anywhere inside the class. If you do not add this as an object's attribute you will not be able to access that value in any function outside of the place where you declared the variable.

Therefore you must ask yourself a question: do I need to access this variable anywhere in my class outside of the scope where I am defining the variable or not? This means in another function inside the class but also in the main script by calling on the instance attribute.

If you do not need to access it - there is no point making it objects attribute.

As for tkinter there are certain objects that you create that if not added as attributes of your class instance they will get garbage collected and destroyed (they will get created and will immediately disappear).

Of the top of my head such objects from tkinter are: StringVar, IntVar and other Var type special variables. Also if you create objects that display images - they (images) have to be added as attributes or they will not display.

Other elements of tkinter like entry or button for example don't perish if not added as an attribute as far as my tests show and can be created without adding them as an attribute. If you have a command in your button though that in any way needs an access to the button object itself you will have to the button first as an instance attribute. (For example one of functions changes the configuration of the button that was pushed to change it's look - then despite that the button will display normally and will not get garbage collected you have to add it as an instance attribute to access it in the function that changes its look.)

Hope it helped you in a small way at least. Happy coding!

Tomasz

\$\endgroup\$
3
  • \$\begingroup\$ This actually really helps me a lot. Having it explained this way is super helpful. So quick question. If for example if I do not assign self to my_image = PhotoImage(file = "myimage.file") and I try to keep a reference to the image with my_label.image = my_image then instead of keeping the image active like it would for a variable outside of a class the garbage collector would still destroy the image because it is not an attribute of the class object? \$\endgroup\$
    – Mike - SMT
    Jun 10, 2017 at 17:05
  • \$\begingroup\$ This assumes that my_label is an instance of a class as you cannot add attributes to other objects, only to instances of a class as far as I know. If you add then an image to your class instance it will be kept as long as that instance is alive. \$\endgroup\$ Jun 10, 2017 at 17:44
  • \$\begingroup\$ By using the dot after a name of an instance of a class you are accessing the class methods or can declare attributes. \$\endgroup\$ Jun 10, 2017 at 17:47
4
\$\begingroup\$

This review is going to be a one note pony...

I went and looked at the previous reviews you have submitted, and a pretty consistent theme, is Python coding conventions, which start with pep8. You clearly are putting in effort to become a better programmer, but the pep8 advice is not sticking. So I am going to assume that is because the utility of same is not apparent.

Here are a few random thoughts to try and sway you... :-)

Wikipedia on Software Quality

We'll start with a wikipedia quote:

Quality

Software peer review frequently involves reading source code. This type of peer review is primarily a defect detection activity. By definition, only the original author of a piece of code has read the source file before the code is submitted for review. Code that is written using consistent guidelines is easier for other reviewers to understand and assimilate, improving the efficacy of the defect detection process.

Q: What is the difference between a Hacker and a SW Engineer?

A: Hackers write code that works, SW Engineers wrote code that someone else can work on.

This is my attempt at a joke to try to illustrate the importance of style consistency. The follow up joke is that someone else can often be yourself when you come back to your own code, after some time away, and look at it and go ouch.

But it is kind of pain.

Yes it is... Until you get good at it. And the best way to get good at something is to practice it.

My number one recommendation is to get a style/lint checker. I use the pycharm ide which will show you style and compile issues right in the editor. It will put a little tick mark in the right hand column, for any violations, and knocking those tick marks off is not as gratifying as getting the code to work, but it can give a sense of accomplishment just the same.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your input. I have been very much working on learning as much as I can on python over the past few months. I did read the pep8 over from my last post and I have tried to remember the conventions but I have been more focused on getting things to work. I will look into a style checker for eclipse as it's the IDE I am using. If I can't find one I can use pycharm as well. \$\endgroup\$
    – Mike - SMT
    Jun 8, 2017 at 2:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.