def create_protocols_tree(): """ 创建协议导航树 :return: 协议导航树 """ protocols_tree.heading('#0', text='选择网络协议', anchor='w') # 参数:parent, index, iid=None, **kw (父节点,插入的位置,id,显示出的文本) # 应用层 applicatoin_layer_tree_entry = protocols_tree.insert("", 0, "应用层", text="应用层") # ""表示父节点是根 http_packet_tree_entry = protocols_tree.insert(applicatoin_layer_tree_entry, 1, "HTTP包", text="HTTP包") dns_packet_tree_entry = protocols_tree.insert(applicatoin_layer_tree_entry, 1, "DNS包", text="DNS包") # 传输层 transfer_layer_tree_entry = protocols_tree.insert("", 1, "传输层", text="传输层") tcp_packet_tree_entry = protocols_tree.insert(transfer_layer_tree_entry, 0, "TCP包", text="TCP包") upd_packet_tree_entry = protocols_tree.insert(transfer_layer_tree_entry, 1, "UDP包", text="UDP包") # 网络层 ip_layer_tree_entry = protocols_tree.insert("", 2, "网络层", text="网络层") ip_packet_tree_entry = protocols_tree.insert(ip_layer_tree_entry, 0, "IP包", text="IP包") icmp_packet_tree_entry = protocols_tree.insert(ip_layer_tree_entry, 1, "ICMP包", text="ICMP包") arp_packet_tree_entry = protocols_tree.insert(ip_layer_tree_entry, 2, "ARP包", text="ARP包") # 网络接入层 ether_layer_tree_entry = protocols_tree.insert("", 3, "网络接入层", text="网络接入层") mac_frame_tree_entry = protocols_tree.insert(ether_layer_tree_entry, 1, "MAC帧", text="MAC帧") protocols_tree.bind('<<TreeviewSelect>>', on_click_protocols_tree) style = Style(tk) # get disabled entry colors disabled_bg = style.lookup("TEntry", "fieldbackground", ("disabled",)) style.map("Treeview", fieldbackground=[("disabled", disabled_bg)], foreground=[("disabled", "gray")], background=[("disabled", disabled_bg)]) protocols_tree.pack() return protocols_tree
def createprotocolsTree(self): """ 生成协议导航树 :return: """ self.protocolsTree.heading('#0', text='选择发送报文类型', anchor='w') # 参数:parent, index, iid=NOne, **kw(父节点,插入的位置,id,显示的文本) # 应用层 applicatoin_layer_tree_entry = self.protocolsTree.insert('', 0, "应用层", text="应用层") dns_packet_tree_entry = self.protocolsTree.insert( applicatoin_layer_tree_entry, 1, "DNS数据包", text="DNS数据包") # 传输层 transfer_layer_tree_entry = self.protocolsTree.insert('', 1, "传输层", text="传输层") tcp_packet_tree_entry = self.protocolsTree.insert( transfer_layer_tree_entry, 0, "TCP数据包", text="TCP数据包") udp_packet_tree_entry = self.protocolsTree.insert( transfer_layer_tree_entry, 1, "UDP数据包", text="UDP数据包") # 网络层协议 ip_layer_tree_entry = self.protocolsTree.insert('', 2, "网络层", text="网络层") arp_packet_tree_entry = self.protocolsTree.insert(ip_layer_tree_entry, 0, "ARP数据包", text="ARP数据包") ip_packet_tree_entry = self.protocolsTree.insert(ip_layer_tree_entry, 1, "IP数据包", text="IP数据包") icmp_packet_tree_entry = self.protocolsTree.insert(ip_layer_tree_entry, 2, "ICMP报文", text="ICMP报文") # 网络接口层协议 ether_layer_tree_entry = self.protocolsTree.insert('', 3, "网络接口层", text="网络接口层") mac_frame_tree_entry = self.protocolsTree.insert( ether_layer_tree_entry, 1, "以太网MAC协议", text="以太网MAC协议") self.protocolsTree.bind('<<TreeviewSelect>>', self.on_click_protocols_tree) style = Style(self) # get disabled entry colors disabled_bg = style.lookup("TEntry", "fieldbackground", ("disabled", )) style.map("Treeview", fieldbackground=[("disabled", disabled_bg)], foreground=[("disabled", "gray")], background=[("disabled", disabled_bg)]) self.protocolsTree.pack() return self.protocolsTree
def __init__(self, master, font, symbols, **kwargs): Toplevel.__init__(self, master, **kwargs) self.title("Symbols") self.grab_set() self.resizable(False, False) self.columnconfigure(0, weight=1) style = Style(self) self.activebg = style.lookup("TEntry", "selectbackground", ("focus", )) self.text_to_insert = "" l = len(symbols) self.canvas = Canvas(self, background="white", width=240, height=(l // 12 + 1) * 20) self.canvas.grid(row=0, column=0, columnspan=2, sticky="eswn") for i, s in enumerate(symbols): x = i % 12 y = i // 12 self.canvas.create_rectangle(x * 20, y * 20, (x + 1) * 20, (y + 1) * 20, activefill=self.activebg, fill="white", width=0, tags="square") self.canvas.create_text(x * 20 + 10, y * 20 + 10, text=s, activefill="white", font="%s 11" % font, tags="char") self.canvas.tag_bind("square", "<Button-1>", self.add_square) self.canvas.tag_bind("square", "<Enter>", self.enter_square) self.canvas.tag_bind("square", "<Leave>", self.leave_square) self.canvas.tag_bind("char", "<Button-1>", self.add_char) self.canvas.tag_bind("char", "<Enter>", self.enter_char) self.canvas.tag_bind("char", "<Leave>", self.leave_char) self.entry = Entry(self) self.entry.grid(row=1, column=0, sticky="ew") self.entry.focus_set() self.entry.bind("<Return>", self.ok) Button(self, text=_("Insert"), width=len(_("Insert")) + 1, command=self.ok).grid(row=1, column=1)
def add_column_headers(self, lab_frame): '''Add the labels for the columns. This needs to be in sync with the DaxModelParameter.render_ui() method. ''' row = self.get_row() stt = Style() lfont = stt.lookup("TLabel", "font") basefont = font.nametofont(lfont) stt.configure("Hdr.TLabel", font=(basefont.cget("family"), basefont.cget("size"), "bold underline")) cols = ["Parameter", "Value", "Frozen?", "Min", "Max", "Units"] for col, txt in enumerate(cols): label = Label(lab_frame, text=txt, style="Hdr.TLabel") label.grid(row=row, column=col) self.next_row()
def __init__(self, master): Toplevel.__init__(self, master, class_=APP_NAME) self.title(_("Settings")) self.grab_set() self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) self.rowconfigure(0, weight=1) self.resizable(True, True) self.minsize(470, 574) style = Style(self) self._bg = style.lookup('TFrame', 'background') self.notebook = Notebook(self) self._validate = self.register(self._validate_entry_nb) self.img_color = PhotoImage(master=self, file=IM_COLOR) self.lang = StringVar(self, LANGUAGES[CONFIG.get("General", "language")]) self.gui = StringVar(self, CONFIG.get("General", "trayicon").capitalize()) self._init_general() self._init_widget() self.notebook.grid(sticky='ewsn', row=0, column=0, columnspan=2) Button(self, text=_('Ok'), command=self.ok).grid(row=1, column=0, sticky='e', padx=4, pady=10) Button(self, text=_('Cancel'), command=self.destroy).grid(row=1, column=1, sticky='w', padx=4, pady=10)
def __init__(self, master, app, **kwargs): """Create category manager.""" Frame.__init__(self, master, padding=4, **kwargs) self.columnconfigure(0, weight=1) self.rowconfigure(1, weight=1) self.app = app self.style = Style(self) self.style.theme_use("clam") self.im_plus = PhotoImage(file=IM_PLUS) self.im_delete = PhotoImage(file=IM_DELETE) # --- Default category self.frame_def_cat = Frame(self) self.default_category = StringVar( self.frame_def_cat, CONFIG.get("General", "default_category").capitalize()) Label(self.frame_def_cat, text=_("Default category ")).grid(row=0, column=0, sticky="e", padx=(4, 0)) self.categories = CONFIG.options("Categories") self.categories.sort() categories = [cat.capitalize() for cat in self.categories] self.def_cat_menu = OptionMenu( self.frame_def_cat, self.default_category, CONFIG.get("General", "default_category").capitalize(), *categories) optionmenu_patch(self.def_cat_menu, self.default_category) self.def_cat_menu.grid(row=0, column=1, sticky="w", padx=4, pady=4) # --- Category colors, names ... style = Style(self) style.configure('txt.TFrame', relief='ridge', border=2) bg = style.lookup('TFrame', 'background') frame = Frame(self, style='txt.TFrame', padding=1) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) txt = Text(frame, width=1, height=1, bg=bg, relief='flat', highlightthickness=0, padx=6, pady=6, cursor='arrow') scroll_x = AutoScrollbar(frame, orient='horizontal', command=txt.xview) scroll_y = AutoScrollbar(frame, orient='vertical', command=txt.yview) txt.configure(xscrollcommand=scroll_x.set, yscrollcommand=scroll_y.set) txt.grid(row=0, column=0, sticky='ewns') scroll_x.grid(row=1, column=0, sticky='ew') scroll_y.grid(row=0, column=1, sticky='ns') self.frame_cat = Frame(txt) txt.window_create('1.0', window=self.frame_cat) txt.configure(state='disabled') self.colors = list(COLORS.keys()) self.colors.sort() self.images = [] self.cat_colors = {} self.cat_labels = {} self.cat_menus = {} self.cat_buttons = {} for i, cat in enumerate(self.categories): self.cat_labels[cat] = Label(self.frame_cat, text="%s" % cat.capitalize(), anchor='e') self.cat_labels[cat].grid(row=i + 2, column=0, sticky="ew", padx=2) self.cat_labels[cat].bind('<Double-Button-1>', self.change_name) self.cat_colors[cat] = StringVar(self) color = CONFIG.get("Categories", cat) self.cat_menus[cat] = OptionMenu(self.frame_cat, self.cat_colors[cat], INV_COLORS[color], *self.colors, command=lambda color, c=cat: self. change_menubutton_color(color, c), style="%s.TMenubutton" % cat) optionmenu_patch(self.cat_menus[cat], self.cat_colors[cat]) self.style.configure("%s.TMenubutton" % cat, background=color) self.cat_menus[cat].grid(row=i + 2, column=1, sticky="w", padx=4, pady=4) self.cat_buttons[cat] = Button( self.frame_cat, image=self.im_delete, padding=0, command=lambda c=cat: self.del_cat(c)) self.cat_buttons[cat].grid(row=i + 2, column=2, padx=4, pady=4, sticky='ns') if len(self.categories) == 1: self.cat_buttons[self.categories[0]].configure(state="disabled") # --- placement self.frame_def_cat.grid(row=0, column=0, sticky="eswn", pady=4) frame.grid(row=1, column=0, sticky="eswn") Button(self, image=self.im_plus, command=self.add_cat).grid(row=2, column=0, sticky='w', pady=8)
def __init__(self, MainWin): """Inits Tkinter window of GridGUI.""" self.root = MainWin #MainWin.geometry("800x600") #You want the size of the app to be 500x500 MainWin.geometry( '+10+30' ) try: style = Style(self.root) if "win" == platform[:3]: style.theme_use('vista') elif "darwin" in platform: style.theme_use('clam') else: style.theme_use('clam') bg = style.lookup("TLabel", "background") self.root.configure(bg=bg) except: print("OOPS... failed to set style.theme_use... Let's press on.") self.MainWin = MainWin MainWin.protocol('WM_DELETE_WINDOW', self.cleanupOnQuit) MainWin.allow_subWindows_to_close = 0 self.add_menu_to_MainWin() topFrame = Frame( MainWin ) # frame for controls #topFrame = tx.ScrolledWindow( MainWin ) frame1 = LabelFrame(topFrame, text="Widgets") self.place_widget_selection_listbox( frame1 ) frame1.pack(anchor=NW, side=LEFT) frame2 = Frame( topFrame ) # frame for radio buttons self.place_gui_definition_controls( frame2, MainWin ) frame2.pack(anchor=N, side=LEFT) self.grid_frame = Frame(topFrame) self.grid_notebook = NotebookGridDes(self, self.grid_frame, MainWin, num_cols=5, num_rows=8) self.grid_frame.pack(anchor=N, side=LEFT) topFrame.pack(fill=BOTH, expand=Y) # make a Status Bar statframe = Frame(MainWin) MainWin.statusMessage = StringVar() MainWin.statusMessage.set('Welcome to TkGridGUI') self.statusbar = Label(statframe, textvariable=MainWin.statusMessage, relief=SUNKEN, anchor=W) self.statusbar.pack(anchor=SW, fill=X, side=BOTTOM) statframe.pack(anchor=SW, fill=X, side=BOTTOM) # Initialize some GridGUI parameters self.current_fileFullName = '' # no file for now self.current_filePath = '' # no file for now self.current_fileName = '' # no file for now self.target_app = TargetTkAppDef( name='myApp') self.PreviewWin = None # need to initialize later self.Listbox_1_Click( 'FromInit' ) # execute selection logic self.in_reading_mode = False # when True, suppresses some automatic trace actions. if len( sys.argv ) == 2: fName = sys.argv[1] if fName.find('.')<0: fName += '.def' fullpath = os.path.abspath(fName) if os.path.isfile( fullpath ): # if file exists, read it as a definition file self.openFile( fName=fullpath ) else: self.MainWin.statusMessage.set('file "%s" does not exist'%fName) self.grid_notebook.notebook.bind("<<NotebookTabChanged>>", self.tab_of_notebook_changed) self.mouse_location = '' self.MainWin.bind("<Enter>", self.onMainWindowEnter)
class Tooltip(Toplevel): """ Tooltip class """ def __init__(self, parent, **kwargs): """ Create a tooltip with given parent. KEYWORD OPTIONS title, alpha, padx, pady, font, background, foreground, image, text """ Toplevel.__init__(self, parent) if 'title' in kwargs: self.title(kwargs['title']) self.transient(parent) self.attributes('-type', 'tooltip') self.attributes('-alpha', kwargs.get('alpha', 0.75)) self.overrideredirect(True) self.configure(padx=kwargs.get('padx', 4)) self.configure(pady=kwargs.get('pady', 4)) self.font = Font(self, kwargs.get('font', '')) self.style = Style(self) if 'background' in kwargs: bg = kwargs['background'] self.configure(background=bg) self.style.configure('tooltip.TLabel', background=bg) if 'foreground' in kwargs: self.style.configure('tooltip.TLabel', foreground=kwargs['foreground']) self.im = kwargs.get('image', None) self.label = Label(self, text=kwargs.get('text', ''), image=self.im, style='tooltip.TLabel', font=self.font, wraplength='6c', compound=kwargs.get('compound', 'left')) self.label.pack() def configure(self, **kwargs): if 'text' in kwargs: self.label.configure(text=kwargs.pop('text')) if 'image' in kwargs: self.label.configure(image=kwargs.pop('image')) if 'background' in kwargs: self.style.configure('tooltip.TLabel', background=kwargs['background']) if 'foreground' in kwargs: fg = kwargs.pop('foreground') self.style.configure('tooltip.TLabel', foreground=fg) if 'alpha' in kwargs: self.attributes('-alpha', kwargs.pop('alpha')) if 'font' in kwargs: font = Font(self, kwargs.pop('font')) self.font.configure(**font.actual()) Toplevel.configure(self, **kwargs) def config(self, **kw): self.configurere(**kw) def cget(self, key): if key in ['text', 'image']: return self.label.cget(key) elif key == 'font': return self.font elif key == 'alpha': return self.attributes('-alpha') elif key in ['foreground', 'background']: return self.style.lookup('tooltip.TLabel', key) else: return Toplevel.cget(self, key)
def __init__(self, parent, length=0, from_=0, to=255, orient='vertical', variable=0, digits=None, tickinterval=None, command=None, style=None, showvalue=True, resolution=1): self.from_ = from_ self.to = to self.variable = variable self.length = length self.command = command self.parent = parent super().__init__(parent, length=length, from_=from_, to=to, orient=orient, variable=variable, command=command, style=style) self.digits = digits self.tickinterval = tickinterval self.showvalue = showvalue self.resolution = resolution # set sliderlength st = Style(self) self.bw_val = bw_val = st.lookup('Vertical.Scale.trough', 'borderwidth') self.sliderlength = sliderlength = 32 if showvalue: self.configure(command=self.display_value) def_font = font.nametofont('TkDefaultFont') # if from_ more than to swap values if from_ < to: pass else: from_, to = to, from_ data = np.arange(from_, (to + 1 if tickinterval >= 1 else to + tickinterval), tickinterval) self.data = data = np.round(data, 1) range_vals = tuple(data) len_rvs = len(range_vals) lspace = def_font.metrics('linespace') len_rvs = len(range_vals) data_size = len_rvs * lspace space_size = len_rvs * 3 sizes = data_size + space_size min_len = (sizes if sizes % 50 == 0 else sizes + 50 - sizes % 50) self.len_val = len_val = min_len if length < min_len else length self.configure(length=len_val) self.rel_min = rel_min = (sliderlength / 2 + bw_val) / len_val self.rel_max = rel_max = 1 - (sliderlength / 2 - bw_val) / len_val if range_vals[-1] == to: pass else: max_rv = range_vals[-1] self.mult_y = mult_y = ((max_rv - from_) * rel_max / (to - from_)) self.bind("<Button-1>", self.resolve) self.build(from_, to, rel_min, rel_max, range_vals, len_rvs)
except ImportError: from Tkinter import Tk from ttk import Style, Button, Label from sys import platform from tkfontchooser import askfont # pip install tkfontchooser # create main window root = Tk() style = Style(root) if "win" == platform[:3]: style.theme_use('vista') elif "darwin" in platform: style.theme_use('clam') else: style.theme_use('clam') bg = style.lookup("TLabel", "background") root.configure(bg=bg) label = Label(root, text='Chosen font: ') label.pack(padx=10, pady=(10,4)) def callback(): # open the font chooser and get the font selected by the user font = askfont(root) # font is "" if the user has cancelled if font: # spaces in the family name need to be escaped font['family'] = font['family'].replace(' ', '\ ') font_str = "%(family)s %(size)i %(weight)s %(slant)s" % font if font['underline']: font_str += ' underline' if font['overstrike']:
class PomodoroParams(Frame): def __init__(self, parent, **options): """ créer le Toplevel permettant de modifier les paramètres """ Frame.__init__(self, parent, **options) self.onglets = Notebook(self) self.onglets.pack(fill='both', expand=True) self.im_color = PhotoImage(master=self, file=IM_COLOR) self.im_plus = PhotoImage(master=self, file=IM_ADD) self.im_moins = PhotoImage(master=self, file=IM_DEL) self.okfct = self.register(only_nb) self.style = Style(self) self.nb_task = len(CONFIG.options("PomodoroTasks")) # --- Général (temps, police et langue) self.general = Frame(self.onglets, padding=10) self.general.columnconfigure(1, weight=1) self.onglets.add(self.general, text=_("General")) # --- --- Temps Label(self.general, text=_("Times (min)"), style='title.TLabel').grid(row=0, pady=4, padx=(2, 10), sticky="w") self.time_frame = Frame(self.general) self.time_frame.grid(row=0, column=1, sticky="w", padx=4) Label(self.time_frame, text=_("Work")).grid(row=0, padx=4, column=0) self.travail = Entry(self.time_frame, width=4, justify='center', validatecommand=(self.okfct, '%P'), validate='key') self.travail.insert(0, CONFIG.get("Pomodoro", "work_time")) self.travail.grid(row=0, column=1, padx=(0, 10)) Label(self.time_frame, text=_("Break")).grid(row=0, column=2, padx=4) self.pause = Entry(self.time_frame, width=4, justify='center', validatecommand=(self.okfct, '%P'), validate='key') self.pause.insert(0, CONFIG.get("Pomodoro", "break_time")) self.pause.grid(row=0, column=3, padx=(0, 10)) Label(self.time_frame, text=_("Rest")).grid(row=0, column=4, padx=4) self.rest = Entry(self.time_frame, width=4, justify='center', validatecommand=(self.okfct, '%P'), validate='key') self.rest.insert(0, CONFIG.get("Pomodoro", "rest_time")) self.rest.grid(row=0, column=5) Separator(self.general, orient='horizontal').grid(row=1, columnspan=2, sticky="ew", pady=10) # --- --- Police Label(self.general, text=_("Font"), style='title.TLabel').grid(row=2, sticky='nw', padx=(2, 10)) self.font = FontFrame(self.general, font=CONFIG.get('Pomodoro', 'font'), sample_text="02:17") self.font.grid(row=2, column=1, padx=4, sticky='w') Separator(self.general, orient='horizontal').grid(row=3, columnspan=2, sticky="ew", pady=10) # --- --- Opacity self.opacity = OpacityFrame(self.general) self.opacity.grid(row=5, columnspan=2, sticky='w', padx=(2, 4), pady=4) Separator(self.general, orient='horizontal').grid(row=6, columnspan=2, sticky="ew", pady=10) # --- --- Son self.sound = SoundFrame(self.general, CONFIG.get("Pomodoro", "beep"), mute=CONFIG.getboolean("Pomodoro", "mute"), label=_("Sound"), style='title.TLabel') self.sound.grid(row=7, columnspan=2, sticky='ew', pady=4) # --- Couleurs self.couleurs = Frame(self.onglets, padding=10) self.couleurs.columnconfigure(3, weight=1) self.onglets.add(self.couleurs, text=_("Colors")) self.bg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "background"), _("Background")) self.work_bg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "work_bg"), _("Background")) self.break_bg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "break_bg"), _("Background")) self.rest_bg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "rest_bg"), _("Background")) self.fg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "foreground"), _("Foreground")) self.work_fg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "work_fg"), _("Foreground")) self.break_fg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "break_fg"), _("Foreground")) self.rest_fg = ColorFrame(self.couleurs, CONFIG.get("Pomodoro", "rest_fg"), _("Foreground")) Label(self.couleurs, text=_("General"), style='title.TLabel').grid(row=0, column=0, pady=4, padx=(2, 10), sticky="w") self.bg.grid(row=0, column=1, sticky='e', padx=8, pady=4) self.fg.grid(row=0, column=2, sticky='e', padx=8, pady=4) Separator(self.couleurs, orient='horizontal').grid(row=1, sticky="ew", pady=10, columnspan=4) Label(self.couleurs, text=_("Work"), style='title.TLabel').grid(row=2, column=0, pady=4, padx=(2, 10), sticky="w") self.work_bg.grid(row=2, column=1, sticky='e', padx=8, pady=4) self.work_fg.grid(row=2, column=2, sticky='e', padx=8, pady=4) Separator(self.couleurs, orient='horizontal').grid(row=3, sticky="ew", pady=10, columnspan=4) Label(self.couleurs, text=_("Break"), style='title.TLabel').grid(row=4, column=0, pady=4, padx=(2, 10), sticky="w") self.break_bg.grid(row=4, column=1, sticky='e', padx=8, pady=4) self.break_fg.grid(row=4, column=2, sticky='e', padx=8, pady=4) Separator(self.couleurs, orient='horizontal').grid(row=5, sticky="ew", pady=10, columnspan=4) Label(self.couleurs, text=_("Rest"), style='title.TLabel').grid(row=6, column=0, pady=4, padx=(2, 10), sticky="w") self.rest_bg.grid(row=6, column=1, sticky='e', padx=8, pady=4) self.rest_fg.grid(row=6, column=2, sticky='e', padx=8, pady=4) # --- Tasks self.stats = Frame(self.onglets, padding=10) self.stats.columnconfigure(0, weight=1) self.stats.rowconfigure(2, weight=1) self.onglets.add(self.stats, text=_("Tasks")) # graph legend legend_frame = Frame(self.stats) Label(legend_frame, style='title.TLabel', text=_('Maximum number of rows in the legend')).pack(side='left') self.legend_row_nb = Entry(legend_frame, width=4, justify='center', validatecommand=(self.okfct, '%P'), validate='key') self.legend_row_nb.insert( 0, CONFIG.get('Pomodoro', 'legend_max_height', fallback='6')) self.legend_row_nb.pack(side='left', padx=4) # task colors can = Canvas(self.stats, bg=self.style.lookup('TFrame', 'background'), highlightthickness=0, width=1, relief='flat') scroll = AutoScrollbar(self.stats, orient='vertical', command=can.yview) can.configure(yscrollcommand=scroll.set) self.task_frame = Frame(can) can.create_window(0, 0, anchor='nw', window=self.task_frame) tasks = CONFIG.options("PomodoroTasks") tasks.sort() cmap = [CONFIG.get("PomodoroTasks", task) for task in tasks] self.tasks = {} self._tasks_btns = {} for i, (coul, task) in enumerate(zip(cmap, tasks)): self.tasks[task] = ColorFrame(self.task_frame, coul, task.capitalize()) self.tasks[task].grid(row=i, column=0, sticky='e', padx=4, pady=4) b = Button(self.task_frame, image=self.im_moins, padding=2, command=lambda t=task: self.del_task(t)) b.grid(row=i, column=1, sticky='w', padx=4, pady=4) self._tasks_btns[task] = b if len(tasks) == 1: self._tasks_btns[tasks[0]].state(['disabled']) legend_frame.grid(row=0, columnspan=2, sticky='w', pady=4) Label(self.stats, text=_('Colors in the statistic graph'), style='title.TLabel').grid(row=1, column=0, sticky='w', pady=4) can.grid(row=2, column=0, sticky='ewns') scroll.grid(row=2, column=1, sticky='ns') Button(self.stats, image=self.im_plus, command=self.add_task).grid(row=3, column=0, sticky='w') self.update_idletasks() can.configure(width=self.task_frame.winfo_reqwidth()) can.configure(scrollregion=can.bbox('all')) can.bind('<4>', lambda e: self._scroll(e, -1)) can.bind('<5>', lambda e: self._scroll(e, 1)) self.task_frame.bind( '<Configure>', lambda e: can.configure(scrollregion=can.bbox('all'))) def _scroll(self, event, delta): if event.widget.yview() != (0, 1): event.widget.yview_scroll(delta, 'units') def choix_couleur(self, type_mode): """ sélection de la couleur du fond/texte pour chaque mode (travail/pause/repos) """ coul = askcolor(self.style.lookup(type_mode + ".TButton", 'background'), parent=self) if coul: self.style.configure(type_mode + ".TButton", background=coul) def coul_stat(self, i): """ choix des couleurs pour l'affichage des stats """ coul = askcolor(self.style.lookup("t%i.TButton" % i, "background"), parent=self) if coul: self.style.configure("t%i.TButton" % i, background=coul) def valide(self): """Update config and return whether the pomodor timer should be stopped.""" old_tpsw = CONFIG.getint("Pomodoro", "work_time") old_tpsp = CONFIG.getint("Pomodoro", "break_time") old_tpsr = CONFIG.getint("Pomodoro", "rest_time") try: tpsw = int(self.travail.get()) except ValueError: # empty entry tpsw = 0 if tpsw == 0: tpsw = old_tpsw try: tpsp = int(self.pause.get()) except ValueError: tpsp = 0 if tpsp == 0: tpsp = old_tpsp try: tpsr = int(self.rest.get()) except ValueError: tpsr = 0 if tpsr == 0: tpsr = old_tpsr sound, mute = self.sound.get() font_prop = self.font.get_font() font = "{} {}".format(font_prop['family'].replace(' ', '\ '), font_prop['size']) try: legend_rows = int(self.legend_row_nb.get()) except ValueError: legend_rows = 0 if legend_rows == 0: legend_rows = CONFIG.getint("Pomodoro", "legend_max_height") if not os.path.exists(sound): showerror( _("Error"), _("The file {filepath} does not exists, the old file will be used." ).format(filepath=sound)) sound = CONFIG.get("Pomodoro", "beep") CONFIG.set("Pomodoro", "alpha", str(self.opacity.get_opacity())) CONFIG.set("Pomodoro", "font", font) CONFIG.set("Pomodoro", "background", self.bg.get_color()) CONFIG.set("Pomodoro", "foreground", self.fg.get_color()) CONFIG.set("Pomodoro", "work_time", str(tpsw)) CONFIG.set("Pomodoro", "work_bg", self.work_bg.get_color()) CONFIG.set("Pomodoro", "work_fg", self.work_fg.get_color()) CONFIG.set("Pomodoro", "break_time", str(tpsp)) CONFIG.set("Pomodoro", "break_bg", self.break_bg.get_color()) CONFIG.set("Pomodoro", "break_fg", self.break_fg.get_color()) CONFIG.set("Pomodoro", "rest_time", str(tpsr)) CONFIG.set("Pomodoro", "rest_bg", self.rest_bg.get_color()) CONFIG.set("Pomodoro", "rest_fg", self.rest_fg.get_color()) CONFIG.set("Pomodoro", "beep", sound) CONFIG.set("Pomodoro", "mute", str(mute)) CONFIG.set("Pomodoro", "legend_max_height", str(legend_rows)) for task, widget in self.tasks.items(): CONFIG.set("PomodoroTasks", task, widget.get_color()) return old_tpsw != tpsw or old_tpsp != tpsp or old_tpsr != old_tpsr def del_task(self, task): """ Suppression de tâches """ rep = askyesno( _("Confirmation"), _("Are you sure you want to delete the task {task}? This action cannot be undone." ).format(task=task.capitalize())) if rep: CONFIG.remove_option("PomodoroTasks", task) # remove stats db = sqlite3.connect(PATH_STATS) cursor = db.cursor() try: cursor.execute('DROP TABLE {}'.format( scrub(task.lower().replace(' ', '_')))) db.commit() except sqlite3.OperationalError: pass # no stats yet db.close() self.tasks[task].destroy() self._tasks_btns[task].destroy() del self.tasks[task] del self._tasks_btns[task] if len(CONFIG.options("PomodoroTasks")) == 1: CONFIG.set("PomodoroTasks", _("Work"), CMAP[0]) self._tasks_btns[CONFIG.options("PomodoroTasks")[0]].state( ['disabled']) save_config() def add_task(self): def ajoute(event=None): task = nom.get().lower().strip() if task in self.tasks: showerror( _("Error"), _("The task {task} already exists.").format(task=task), parent=self) elif task: coul = CMAP[(len(self.tasks) + 1) % len(CMAP)] i = self.task_frame.grid_size()[1] + 1 self.tasks[task] = ColorFrame(self.task_frame, coul, task.capitalize()) self.tasks[task].grid(row=i, column=0, sticky='e', padx=4, pady=4) b = Button(self.task_frame, image=self.im_moins, padding=2, command=lambda t=task: self.del_task(t)) b.grid(row=i, column=1, sticky='w', padx=4, pady=4) self._tasks_btns[task] = b self._tasks_btns[CONFIG.options("PomodoroTasks")[0]].state( ['!disabled']) top.destroy() top = Toplevel(self) top.title(_("New task")) top.transient(self) top.grab_set() nom = Entry(top, width=20, justify='center') nom.grid(row=0, columnspan=2, sticky="ew") nom.focus_set() nom.bind('<Key-Return>', ajoute) Button(top, text=_("Cancel"), command=top.destroy).grid(row=1, column=0) Button(top, text=_("Ok"), command=ajoute).grid(row=1, column=1) top.wait_window(top)
def __init__(self, master, columns, data=None, command=None, sort=True, select_mode=None, heading_anchor=CENTER, cell_anchor=W, style=None, height=None, padding=None, adjust_heading_to_content=False, stripped_rows=None, selection_background=None, selection_foreground=None, field_background=None, heading_font=None, heading_background=None, heading_foreground=None, cell_pady=2, cell_background=None, cell_foreground=None, cell_font=None, headers=True): self._stripped_rows = stripped_rows self._columns = columns self._number_of_rows = 0 self._number_of_columns = len(columns) self.row = self.List_Of_Rows(self) self.column = self.List_Of_Columns(self) s = Style() if style is None: style_name = "Multicolumn_Listbox%s.Treeview" % self._style_index self._style_index += 1 else: style_name = style style_map = {} if selection_background is not None: style_map["background"] = [('selected', selection_background)] if selection_foreground is not None: style_map["foeground"] = [('selected', selection_foreground)] if style_map: s.map(style_name, **style_map) style_config = {} if cell_background is not None: style_config["background"] = cell_background if cell_foreground is not None: style_config["foreground"] = cell_foreground if cell_font is None: font_name = s.lookup(style_name, "font") cell_font = nametofont(font_name) else: if not isinstance(cell_font, Font): if isinstance(cell_font, basestring): cell_font = nametofont(cell_font) else: if len(Font) == 1: cell_font = Font(family=cell_font[0]) elif len(Font) == 2: cell_font = Font(family=cell_font[0], size=cell_font[1]) elif len(Font) == 3: cell_font = Font(family=cell_font[0], size=cell_font[1], weight=cell_font[2]) else: raise ValueError( "Not possible more than 3 values for font") style_config["font"] = cell_font self._cell_font = cell_font self._rowheight = cell_font.metrics("linespace") + cell_pady style_config["rowheight"] = self._rowheight if field_background is not None: style_config["fieldbackground"] = field_background s.configure(style_name, **style_config) heading_style_config = {} if heading_font is not None: heading_style_config["font"] = heading_font if heading_background is not None: heading_style_config["background"] = heading_background if heading_foreground is not None: heading_style_config["foreground"] = heading_foreground heading_style_name = style_name + ".Heading" s.configure(heading_style_name, **heading_style_config) treeview_kwargs = {"style": style_name} if height is not None: treeview_kwargs["height"] = height if padding is not None: treeview_kwargs["padding"] = padding if headers: treeview_kwargs["show"] = "headings" else: treeview_kwargs["show"] = "" if select_mode is not None: treeview_kwargs["selectmode"] = select_mode self.interior = Treeview(master, columns=columns, **treeview_kwargs) if command is not None: self._command = command self.interior.bind("<<TreeviewSelect>>", self._on_select) for i in range(0, self._number_of_columns): if sort: self.interior.heading( i, text=columns[i], anchor=heading_anchor, command=lambda col=i: self.sort_by(col, descending=False)) else: self.interior.heading(i, text=columns[i], anchor=heading_anchor) if adjust_heading_to_content: self.interior.column(i, width=Font().measure(columns[i])) self.interior.column(i, anchor=cell_anchor) if data is not None: for row in data: self.insert_row(row)
class DefaultThemingEngine(AOSLibrary): def __init__(self, manager): super().__init__(manager) self.manager.registerConfig('theme') self.style = Style() self.style.configure("Tk", background="#000000", foreground="white") self.delayEnable = True def enable(self): self.manager.addTool('Edit Theme', self.themeList) self.loadTheme() def disable(self): self.manager.removeTool('Edit Theme') def themeList(self): self.themelistwin = Toplevel() Label(self.themelistwin, text="Log Window background:").grid(row=0, column=0) self.textbox_bg = Entry(self.themelistwin) self.textbox_bg.grid(row=0, column=1) self.textbox_bg.insert(0, self.manager.process.log.textbox['background']) Label(self.themelistwin, text="Log Window Font Colour:").grid(row=0, column=2) self.textbox_fg = Entry(self.themelistwin) self.textbox_fg.grid(row=0, column=3) self.textbox_fg.insert(0, self.manager.process.log.textbox['foreground']) Label(self.themelistwin, text="Buttons background:").grid(row=1, column=0) self.button_bg = Entry(self.themelistwin) self.button_bg.grid(row=1, column=1) self.button_bg.insert(0, self.style.lookup("TButton", "background")) Label(self.themelistwin, text="Buttons padding:").grid(row=2, column=0) self.button_fbg = Entry(self.themelistwin) self.button_fbg.grid(row=2, column=1) self.button_fbg.insert(0, self.style.lookup("TButton", "padding")) Label(self.themelistwin, text="Buttons foreground:").grid(row=3, column=0) self.button_fg = Entry(self.themelistwin) self.button_fg.grid(row=3, column=1) self.button_fg.insert(0, self.style.lookup("TButton", "foreground")) Label(self.themelistwin, text="Buttons relief:").grid(row=4, column=0) self.button_rf = Entry(self.themelistwin) self.button_rf.grid(row=4, column=1) self.button_rf.insert(0, self.style.lookup("TButton", "relief")) Button(self.themelistwin, text="Save", command=self.saveTheme).grid(row=10, column=0) self.themelistwin.bind('<Return>', lambda evt: self.saveTheme()) def saveTheme(self): self.manager.getConfig('theme')._set('theme_outputwin', self.textbox_bg.get()) self.manager.getConfig('theme')._set('theme_outputwin_font', self.textbox_fg.get()) self.manager.process.log.textbox.configure(bg=self.textbox_bg.get()) self.manager.process.log.textbox.configure(fg=self.textbox_fg.get()) self.style.configure("TButton", padding=self.button_fbg.get(), relief=self.button_rf.get(), background=self.button_bg.get(), foreground=self.button_fg.get()) data = { 'button': { 'padding': self.button_fbg.get(), 'relief': self.button_rf.get(), 'background': self.button_bg.get(), 'foreground': self.button_fg.get() } } self.manager.getConfig('theme')._set('theme', data) def loadTheme(self): tbb = self.manager.getConfig('theme').get( 'theme_outputwin', self.manager.process.log.textbox['background']) tbf = self.manager.getConfig('theme').get( 'theme_outputwin_font', self.manager.process.log.textbox['foreground']) self.manager.process.log.textbox.configure(bg=tbb) self.manager.process.log.textbox.configure(fg=tbf) data = self.manager.getConfig('theme').get('theme', {}) if data: theme_b = data.get('button', None) #print(theme_b) if theme_b: #print(theme_b.get('background')) self.style.configure( "TButton", padding=theme_b.get( 'padding', self.style.lookup("TButton", "padding")), relief=theme_b.get('relief', self.style.lookup("TButton", "relief")), background=theme_b.get( 'background', self.style.lookup("TButton", "background")), foreground=theme_b.get( 'foreground', self.style.lookup("TButton", "foreground")))
subprocess.Popen([save_to], shell=True) except Exception: print("Fichier github_id absent") if __name__ == '__main__': MIN_WIDTH: int = 400 MIN_HEIGHT: int = 150 window = ThemedTk(theme='plastik') # arc, breeze, plastik, clearlooks, black, equilux window.title('Utilitaires TMA') window.resizable(False, False) # Personnalisation Style style = Style(window) original_font = font.nametofont(style.lookup("TLabel", "font")) f = font.Font(**original_font.configure()) f.configure(weight='bold', underline=0, size=9) style.configure('H1.TLabel', font=f) original_font = font.nametofont(style.lookup("TButton", "font")) f2 = font.Font(**original_font.configure()) f2.configure(weight='bold', size=9) style.configure('H1.TButton', font=f2) window.grid_columnconfigure(0, weight=1) window.grid_rowconfigure(0, weight=1) window.minsize(MIN_WIDTH, MIN_HEIGHT) # FRAME Principale frame_principale = Frame(window)
class EventScheduler(Tk): def __init__(self): Tk.__init__(self, className='Scheduler') logging.info('Start') self.protocol("WM_DELETE_WINDOW", self.hide) self._visible = BooleanVar(self, False) self.withdraw() self.icon_img = PhotoImage(master=self, file=ICON48) self.iconphoto(True, self.icon_img) # --- systray icon self.icon = TrayIcon(ICON, fallback_icon_path=ICON_FALLBACK) # --- menu self.menu_widgets = SubMenu(parent=self.icon.menu) self.menu_eyes = Eyes(self.icon.menu, self) self.icon.menu.add_checkbutton(label=_('Manager'), command=self.display_hide) self.icon.menu.add_cascade(label=_('Widgets'), menu=self.menu_widgets) self.icon.menu.add_cascade(label=_("Eyes' rest"), menu=self.menu_eyes) self.icon.menu.add_command(label=_('Settings'), command=self.settings) self.icon.menu.add_separator() self.icon.menu.add_command(label=_('About'), command=lambda: About(self)) self.icon.menu.add_command(label=_('Quit'), command=self.exit) self.icon.bind_left_click(lambda: self.display_hide(toggle=True)) add_trace(self._visible, 'write', self._visibility_trace) self.menu = Menu(self, tearoff=False) self.menu.add_command(label=_('Edit'), command=self._edit_menu) self.menu.add_command(label=_('Delete'), command=self._delete_menu) self.right_click_iid = None self.menu_task = Menu(self.menu, tearoff=False) self._task_var = StringVar(self) menu_in_progress = Menu(self.menu_task, tearoff=False) for i in range(0, 110, 10): prog = '{}%'.format(i) menu_in_progress.add_radiobutton(label=prog, value=prog, variable=self._task_var, command=self._set_progress) for state in ['Pending', 'Completed', 'Cancelled']: self.menu_task.add_radiobutton(label=_(state), value=state, variable=self._task_var, command=self._set_progress) self._img_dot = tkPhotoImage(master=self) self.menu_task.insert_cascade(1, menu=menu_in_progress, compound='left', label=_('In Progress'), image=self._img_dot) self.title('Scheduler') self.rowconfigure(1, weight=1) self.columnconfigure(0, weight=1) self.scheduler = BackgroundScheduler(coalesce=False, misfire_grace_time=86400) self.scheduler.add_jobstore('sqlalchemy', url='sqlite:///%s' % JOBSTORE) self.scheduler.add_jobstore('memory', alias='memo') # --- style self.style = Style(self) self.style.theme_use("clam") self.style.configure('title.TLabel', font='TkdefaultFont 10 bold') self.style.configure('title.TCheckbutton', font='TkdefaultFont 10 bold') self.style.configure('subtitle.TLabel', font='TkdefaultFont 9 bold') self.style.configure('white.TLabel', background='white') self.style.configure('border.TFrame', background='white', border=1, relief='sunken') self.style.configure("Treeview.Heading", font="TkDefaultFont") bgc = self.style.lookup("TButton", "background") fgc = self.style.lookup("TButton", "foreground") bga = self.style.lookup("TButton", "background", ("active", )) self.style.map('TCombobox', fieldbackground=[('readonly', 'white'), ('readonly', 'focus', 'white')], background=[("disabled", "active", "readonly", bgc), ("!disabled", "active", "readonly", bga)], foreground=[('readonly', '!disabled', fgc), ('readonly', '!disabled', 'focus', fgc), ('readonly', 'disabled', 'gray40'), ('readonly', 'disabled', 'focus', 'gray40') ], arrowcolor=[("disabled", "gray40")]) self.style.configure('menu.TCombobox', foreground=fgc, background=bgc, fieldbackground=bgc) self.style.map('menu.TCombobox', fieldbackground=[('readonly', bgc), ('readonly', 'focus', bgc)], background=[("disabled", "active", "readonly", bgc), ("!disabled", "active", "readonly", bga)], foreground=[('readonly', '!disabled', fgc), ('readonly', '!disabled', 'focus', fgc), ('readonly', 'disabled', 'gray40'), ('readonly', 'disabled', 'focus', 'gray40') ], arrowcolor=[("disabled", "gray40")]) self.style.map('DateEntry', arrowcolor=[("disabled", "gray40")]) self.style.configure('cal.TFrame', background='#424242') self.style.configure('month.TLabel', background='#424242', foreground='white') self.style.configure('R.TButton', background='#424242', arrowcolor='white', bordercolor='#424242', lightcolor='#424242', darkcolor='#424242') self.style.configure('L.TButton', background='#424242', arrowcolor='white', bordercolor='#424242', lightcolor='#424242', darkcolor='#424242') active_bg = self.style.lookup('TEntry', 'selectbackground', ('focus', )) self.style.map('R.TButton', background=[('active', active_bg)], bordercolor=[('active', active_bg)], darkcolor=[('active', active_bg)], lightcolor=[('active', active_bg)]) self.style.map('L.TButton', background=[('active', active_bg)], bordercolor=[('active', active_bg)], darkcolor=[('active', active_bg)], lightcolor=[('active', active_bg)]) self.style.configure('txt.TFrame', background='white') self.style.layout('down.TButton', [('down.TButton.downarrow', { 'side': 'right', 'sticky': 'ns' })]) self.style.map('TRadiobutton', indicatorforeground=[('disabled', 'gray40')]) self.style.map('TCheckbutton', indicatorforeground=[('disabled', 'gray40')], indicatorbackground=[ ('pressed', '#dcdad5'), ('!disabled', 'alternate', 'white'), ('disabled', 'alternate', '#a0a0a0'), ('disabled', '#dcdad5') ]) self.style.map('down.TButton', arrowcolor=[("disabled", "gray40")]) self.style.map('TMenubutton', arrowcolor=[('disabled', self.style.lookup('TMenubutton', 'foreground', ['disabled']))]) bg = self.style.lookup('TFrame', 'background', default='#ececec') self.configure(bg=bg) self.option_add('*Toplevel.background', bg) self.option_add('*Menu.background', bg) self.option_add('*Menu.tearOff', False) # toggle text self._open_image = PhotoImage(name='img_opened', file=IM_OPENED, master=self) self._closed_image = PhotoImage(name='img_closed', file=IM_CLOSED, master=self) self._open_image_sel = PhotoImage(name='img_opened_sel', file=IM_OPENED_SEL, master=self) self._closed_image_sel = PhotoImage(name='img_closed_sel', file=IM_CLOSED_SEL, master=self) self.style.element_create( "toggle", "image", "img_closed", ("selected", "!disabled", "img_opened"), ("active", "!selected", "!disabled", "img_closed_sel"), ("active", "selected", "!disabled", "img_opened_sel"), border=2, sticky='') self.style.map('Toggle', background=[]) self.style.layout('Toggle', [('Toggle.border', { 'children': [('Toggle.padding', { 'children': [('Toggle.toggle', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) # toggle sound self._im_sound = PhotoImage(master=self, file=IM_SOUND) self._im_mute = PhotoImage(master=self, file=IM_MUTE) self._im_sound_dis = PhotoImage(master=self, file=IM_SOUND_DIS) self._im_mute_dis = PhotoImage(master=self, file=IM_MUTE_DIS) self.style.element_create( 'mute', 'image', self._im_sound, ('selected', '!disabled', self._im_mute), ('selected', 'disabled', self._im_mute_dis), ('!selected', 'disabled', self._im_sound_dis), border=2, sticky='') self.style.layout('Mute', [('Mute.border', { 'children': [('Mute.padding', { 'children': [('Mute.mute', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('Mute', relief='raised') # widget scrollbar self._im_trough = {} self._im_slider_vert = {} self._im_slider_vert_prelight = {} self._im_slider_vert_active = {} self._slider_alpha = Image.open(IM_SCROLL_ALPHA) for widget in ['Events', 'Tasks']: bg = CONFIG.get(widget, 'background', fallback='gray10') fg = CONFIG.get(widget, 'foreground') widget_bg = self.winfo_rgb(bg) widget_fg = tuple( round(c * 255 / 65535) for c in self.winfo_rgb(fg)) active_bg = active_color(*widget_bg) active_bg2 = active_color(*active_color(*widget_bg, 'RGB')) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) self._im_trough[widget] = tkPhotoImage(width=15, height=15, master=self) self._im_trough[widget].put(" ".join( ["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active[widget] = PhotoImage( slider_vert_active, master=self) self._im_slider_vert[widget] = PhotoImage(slider_vert, master=self) self._im_slider_vert_prelight[widget] = PhotoImage( slider_vert_prelight, master=self) self.style.element_create('%s.Vertical.Scrollbar.trough' % widget, 'image', self._im_trough[widget]) self.style.element_create( '%s.Vertical.Scrollbar.thumb' % widget, 'image', self._im_slider_vert[widget], ('pressed', '!disabled', self._im_slider_vert_active[widget]), ('active', '!disabled', self._im_slider_vert_prelight[widget]), border=6, sticky='ns') self.style.layout( '%s.Vertical.TScrollbar' % widget, [('%s.Vertical.Scrollbar.trough' % widget, { 'children': [('%s.Vertical.Scrollbar.thumb' % widget, { 'expand': '1' })], 'sticky': 'ns' })]) # --- tree columns = { _('Summary'): ({ 'stretch': True, 'width': 300 }, lambda: self._sort_by_desc(_('Summary'), False)), _('Place'): ({ 'stretch': True, 'width': 200 }, lambda: self._sort_by_desc(_('Place'), False)), _('Start'): ({ 'stretch': False, 'width': 150 }, lambda: self._sort_by_date(_('Start'), False)), _('End'): ({ 'stretch': False, 'width': 150 }, lambda: self._sort_by_date(_("End"), False)), _('Category'): ({ 'stretch': False, 'width': 100 }, lambda: self._sort_by_desc(_('Category'), False)) } self.tree = Treeview(self, show="headings", columns=list(columns)) for label, (col_prop, cmd) in columns.items(): self.tree.column(label, **col_prop) self.tree.heading(label, text=label, anchor="w", command=cmd) self.tree.tag_configure('0', background='#ececec') self.tree.tag_configure('1', background='white') self.tree.tag_configure('outdated', foreground='red') scroll = AutoScrollbar(self, orient='vertical', command=self.tree.yview) self.tree.configure(yscrollcommand=scroll.set) # --- toolbar toolbar = Frame(self) self.img_plus = PhotoImage(master=self, file=IM_ADD) Button(toolbar, image=self.img_plus, padding=1, command=self.add).pack(side="left", padx=4) Label(toolbar, text=_("Filter by")).pack(side="left", padx=4) # --- TODO: add filter by start date (after date) self.filter_col = Combobox( toolbar, state="readonly", # values=("",) + self.tree.cget('columns')[1:], values=("", _("Category")), exportselection=False) self.filter_col.pack(side="left", padx=4) self.filter_val = Combobox(toolbar, state="readonly", exportselection=False) self.filter_val.pack(side="left", padx=4) Button(toolbar, text=_('Delete All Outdated'), padding=1, command=self.delete_outdated_events).pack(side="right", padx=4) # --- grid toolbar.grid(row=0, columnspan=2, sticky='we', pady=4) self.tree.grid(row=1, column=0, sticky='eswn') scroll.grid(row=1, column=1, sticky='ns') # --- restore data data = {} self.events = {} self.nb = 0 try: with open(DATA_PATH, 'rb') as file: dp = Unpickler(file) data = dp.load() except Exception: l = [ f for f in os.listdir(os.path.dirname(BACKUP_PATH)) if f.startswith('data.backup') ] if l: l.sort(key=lambda x: int(x[11:])) shutil.copy(os.path.join(os.path.dirname(BACKUP_PATH), l[-1]), DATA_PATH) with open(DATA_PATH, 'rb') as file: dp = Unpickler(file) data = dp.load() self.nb = len(data) backup() now = datetime.now() for i, prop in enumerate(data): iid = str(i) self.events[iid] = Event(self.scheduler, iid=iid, **prop) self.tree.insert('', 'end', iid, values=self.events[str(i)].values()) tags = [str(self.tree.index(iid) % 2)] self.tree.item(iid, tags=tags) if not prop['Repeat']: for rid, d in list(prop['Reminders'].items()): if d < now: del self.events[iid]['Reminders'][rid] self.after_id = self.after(15 * 60 * 1000, self.check_outdated) # --- bindings self.bind_class("TCombobox", "<<ComboboxSelected>>", self.clear_selection, add=True) self.bind_class("TCombobox", "<Control-a>", self.select_all) self.bind_class("TEntry", "<Control-a>", self.select_all) self.tree.bind('<3>', self._post_menu) self.tree.bind('<1>', self._select) self.tree.bind('<Double-1>', self._edit_on_click) self.menu.bind('<FocusOut>', lambda e: self.menu.unpost()) self.filter_col.bind("<<ComboboxSelected>>", self.update_filter_val) self.filter_val.bind("<<ComboboxSelected>>", self.apply_filter) # --- widgets self.widgets = {} prop = { op: CONFIG.get('Calendar', op) for op in CONFIG.options('Calendar') } self.widgets['Calendar'] = CalendarWidget(self, locale=CONFIG.get( 'General', 'locale'), **prop) self.widgets['Events'] = EventWidget(self) self.widgets['Tasks'] = TaskWidget(self) self.widgets['Timer'] = Timer(self) self.widgets['Pomodoro'] = Pomodoro(self) self._setup_style() for item, widget in self.widgets.items(): self.menu_widgets.add_checkbutton( label=_(item), command=lambda i=item: self.display_hide_widget(i)) self.menu_widgets.set_item_value(_(item), widget.variable.get()) add_trace(widget.variable, 'write', lambda *args, i=item: self._menu_widgets_trace(i)) self.icon.loop(self) self.tk.eval(""" apply {name { set newmap {} foreach {opt lst} [ttk::style map $name] { if {($opt eq "-foreground") || ($opt eq "-background")} { set newlst {} foreach {st val} $lst { if {($st eq "disabled") || ($st eq "selected")} { lappend newlst $st $val } } if {$newlst ne {}} { lappend newmap $opt $newlst } } else { lappend newmap $opt $lst } } ttk::style map $name {*}$newmap }} Treeview """) # react to scheduler --update-date in command line signal.signal(signal.SIGUSR1, self.update_date) # update selected date in calendar and event list every day self.scheduler.add_job(self.update_date, CronTrigger(hour=0, minute=0, second=1), jobstore='memo') self.scheduler.start() def _setup_style(self): # scrollbars for widget in ['Events', 'Tasks']: bg = CONFIG.get(widget, 'background', fallback='gray10') fg = CONFIG.get(widget, 'foreground', fallback='white') widget_bg = self.winfo_rgb(bg) widget_fg = tuple( round(c * 255 / 65535) for c in self.winfo_rgb(fg)) active_bg = active_color(*widget_bg) active_bg2 = active_color(*active_color(*widget_bg, 'RGB')) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert.putalpha(self._slider_alpha) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_active.putalpha(self._slider_alpha) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) slider_vert_prelight.putalpha(self._slider_alpha) self._im_trough[widget].put(" ".join( ["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active[widget].paste(slider_vert_active) self._im_slider_vert[widget].paste(slider_vert) self._im_slider_vert_prelight[widget].paste(slider_vert_prelight) for widget in self.widgets.values(): widget.update_style() def report_callback_exception(self, *args): err = ''.join(traceback.format_exception(*args)) logging.error(err) showerror('Exception', str(args[1]), err, parent=self) def save(self): logging.info('Save event database') data = [ev.to_dict() for ev in self.events.values()] with open(DATA_PATH, 'wb') as file: pick = Pickler(file) pick.dump(data) def update_date(self, *args): """Update Calendar's selected day and Events' list.""" self.widgets['Calendar'].update_date() self.widgets['Events'].display_evts() self.update_idletasks() # --- bindings def _select(self, event): if not self.tree.identify_row(event.y): self.tree.selection_remove(*self.tree.selection()) def _edit_on_click(self, event): sel = self.tree.selection() if sel: sel = sel[0] self.edit(sel) # --- class bindings @staticmethod def clear_selection(event): combo = event.widget combo.selection_clear() @staticmethod def select_all(event): event.widget.selection_range(0, "end") return "break" # --- show / hide def _menu_widgets_trace(self, item): self.menu_widgets.set_item_value(_(item), self.widgets[item].variable.get()) def display_hide_widget(self, item): value = self.menu_widgets.get_item_value(_(item)) if value: self.widgets[item].show() else: self.widgets[item].hide() def hide(self): self._visible.set(False) self.withdraw() self.save() def show(self): self._visible.set(True) self.deiconify() def _visibility_trace(self, *args): self.icon.menu.set_item_value(_('Manager'), self._visible.get()) def display_hide(self, toggle=False): value = self.icon.menu.get_item_value(_('Manager')) if toggle: value = not value self.icon.menu.set_item_value(_('Manager'), value) self._visible.set(value) if not value: self.withdraw() self.save() else: self.deiconify() # --- event management def event_add(self, event): self.nb += 1 iid = str(self.nb) self.events[iid] = event self.tree.insert('', 'end', iid, values=event.values()) self.tree.item(iid, tags=str(self.tree.index(iid) % 2)) self.widgets['Calendar'].add_event(event) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def event_configure(self, iid): self.tree.item(iid, values=self.events[iid].values()) self.widgets['Calendar'].add_event(self.events[iid]) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def add(self, date=None): iid = str(self.nb + 1) if date is not None: event = Event(self.scheduler, iid=iid, Start=date) else: event = Event(self.scheduler, iid=iid) Form(self, event, new=True) def delete(self, iid): index = self.tree.index(iid) self.tree.delete(iid) for k, item in enumerate(self.tree.get_children('')[index:]): tags = [ t for t in self.tree.item(item, 'tags') if t not in ['1', '0'] ] tags.append(str((index + k) % 2)) self.tree.item(item, tags=tags) self.events[iid].reminder_remove_all() self.widgets['Calendar'].remove_event(self.events[iid]) del (self.events[iid]) self.widgets['Events'].display_evts() self.widgets['Tasks'].display_tasks() self.save() def edit(self, iid): self.widgets['Calendar'].remove_event(self.events[iid]) Form(self, self.events[iid]) def check_outdated(self): """Check for outdated events every 15 min.""" now = datetime.now() for iid, event in self.events.items(): if not event['Repeat'] and event['Start'] < now: tags = list(self.tree.item(iid, 'tags')) if 'outdated' not in tags: tags.append('outdated') self.tree.item(iid, tags=tags) self.after_id = self.after(15 * 60 * 1000, self.check_outdated) def delete_outdated_events(self): now = datetime.now() outdated = [] for iid, prop in self.events.items(): if prop['End'] < now: if not prop['Repeat']: outdated.append(iid) elif prop['Repeat']['Limit'] != 'always': end = prop['End'] enddate = datetime.fromordinal( prop['Repeat']['EndDate'].toordinal()) enddate.replace(hour=end.hour, minute=end.minute) if enddate < now: outdated.append(iid) for item in outdated: self.delete(item) logging.info('Deleted outdated events') def refresh_reminders(self): """ Reschedule all reminders. Required when APScheduler is updated. """ for event in self.events.values(): reminders = [date for date in event['Reminders'].values()] event.reminder_remove_all() for date in reminders: event.reminder_add(date) logging.info('Refreshed reminders') # --- sorting def _move_item(self, item, index): self.tree.move(item, "", index) tags = [t for t in self.tree.item(item, 'tags') if t not in ['1', '0']] tags.append(str(index % 2)) self.tree.item(item, tags=tags) @staticmethod def to_datetime(date): date_format = get_date_format("short", CONFIG.get("General", "locale")).pattern dayfirst = date_format.startswith("d") yearfirst = date_format.startswith("y") return parse(date, dayfirst=dayfirst, yearfirst=yearfirst) def _sort_by_date(self, col, reverse): l = [(self.to_datetime(self.tree.set(k, col)), k) for k in self.tree.get_children('')] l.sort(reverse=reverse) # rearrange items in sorted positions for index, (val, k) in enumerate(l): self._move_item(k, index) # reverse sort next time self.tree.heading(col, command=lambda: self._sort_by_date(col, not reverse)) def _sort_by_desc(self, col, reverse): l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')] l.sort(reverse=reverse, key=lambda x: x[0].lower()) # rearrange items in sorted positions for index, (val, k) in enumerate(l): self._move_item(k, index) # reverse sort next time self.tree.heading(col, command=lambda: self._sort_by_desc(col, not reverse)) # --- filter def update_filter_val(self, event): col = self.filter_col.get() self.filter_val.set("") if col: l = set() for k in self.events: l.add(self.tree.set(k, col)) self.filter_val.configure(values=tuple(l)) else: self.filter_val.configure(values=[]) self.apply_filter(event) def apply_filter(self, event): col = self.filter_col.get() val = self.filter_val.get() items = list(self.events.keys()) if not col: for item in items: self._move_item(item, int(item)) else: i = 0 for item in items: if self.tree.set(item, col) == val: self._move_item(item, i) i += 1 else: self.tree.detach(item) # --- manager's menu def _post_menu(self, event): self.right_click_iid = self.tree.identify_row(event.y) self.tree.selection_remove(*self.tree.selection()) self.tree.selection_add(self.right_click_iid) if self.right_click_iid: try: self.menu.delete(_('Progress')) except TclError: pass state = self.events[self.right_click_iid]['Task'] if state: self._task_var.set(state) if '%' in state: self._img_dot = PhotoImage(master=self, file=IM_DOT) else: self._img_dot = tkPhotoImage(master=self) self.menu_task.entryconfigure(1, image=self._img_dot) self.menu.insert_cascade(0, menu=self.menu_task, label=_('Progress')) self.menu.tk_popup(event.x_root, event.y_root) def _delete_menu(self): if self.right_click_iid: self.delete(self.right_click_iid) def _edit_menu(self): if self.right_click_iid: self.edit(self.right_click_iid) def _set_progress(self): if self.right_click_iid: self.events[self.right_click_iid]['Task'] = self._task_var.get() self.widgets['Tasks'].display_tasks() if '%' in self._task_var.get(): self._img_dot = PhotoImage(master=self, file=IM_DOT) else: self._img_dot = tkPhotoImage(master=self) self.menu_task.entryconfigure(1, image=self._img_dot) # --- icon menu def exit(self): self.save() rep = self.widgets['Pomodoro'].stop(self.widgets['Pomodoro'].on) if not rep: return self.menu_eyes.quit() self.after_cancel(self.after_id) try: self.scheduler.shutdown() except SchedulerNotRunningError: pass self.destroy() def settings(self): splash_supp = CONFIG.get('General', 'splash_supported', fallback=True) dialog = Settings(self) self.wait_window(dialog) self._setup_style() if splash_supp != CONFIG.get('General', 'splash_supported'): for widget in self.widgets.values(): widget.update_position() # --- week schedule def get_next_week_events(self): """Return events scheduled for the next 7 days """ locale = CONFIG.get("General", "locale") next_ev = {} today = datetime.now().date() for d in range(7): day = today + timedelta(days=d) evts = self.widgets['Calendar'].get_events(day) if evts: evts = [self.events[iid] for iid in evts] evts.sort(key=lambda ev: ev.get_start_time()) desc = [] for ev in evts: if ev["WholeDay"]: date = "" else: date = "%s - %s " % ( format_time(ev['Start'], locale=locale), format_time(ev['End'], locale=locale)) place = "(%s)" % ev['Place'] if place == "()": place = "" desc.append(("%s%s %s\n" % (date, ev['Summary'], place), ev['Description'])) next_ev[day.strftime('%A')] = desc return next_ev # --- tasks def get_tasks(self): # TODO: find events with repetition in the week # TODO: better handling of events on several days tasks = [] for event in self.events.values(): if event['Task']: tasks.append(event) return tasks
def __init__(self, master, event, new=False): Toplevel.__init__(self, master) self.minsize(410, 402) if master.winfo_ismapped(): self.transient(master) self.protocol('WM_DELETE_WINDOW', self.cancel) self._only_nb = self.register(only_nb) self.event = event if new: self.title(_('New Event')) else: self.title(_('Edit Event')) self._new = new self._task = BooleanVar(self, bool(event['Task'])) self._whole_day = BooleanVar(self, event['WholeDay']) # --- style style = Style(self) active_bg = style.lookup('TEntry', 'selectbackground', ('focus', )) self.alarms = [] notebook = Notebook(self) notebook.pack(fill='both', expand=True) Button(self, text=_('Ok'), command=self.ok).pack(pady=(10, 6), padx=4) # --- event settings frame_event = Frame(notebook) notebook.add(frame_event, text=_('Event'), sticky='eswn', padding=4) frame_event.columnconfigure(1, weight=1) frame_event.rowconfigure(5, weight=1) self.img_moins = PhotoImage(master=self, file=IM_DEL) self.img_bell = PhotoImage(master=self, file=IM_BELL) Label(frame_event, text=_('Summary')).grid(row=0, column=0, padx=4, pady=6, sticky='e') Label(frame_event, text=_('Place')).grid(row=1, column=0, padx=4, pady=6, sticky='e') Label(frame_event, text=_('Start')).grid(row=2, column=0, padx=4, pady=6, sticky='e') self._end_label = Label(frame_event, text=_('End')) self._end_label.grid(row=3, column=0, padx=4, pady=6, sticky='e') frame_task = Frame(frame_event) frame_task.grid(row=4, column=1, padx=4, pady=6, sticky='w') Label(frame_event, text=_('Description')).grid(row=5, column=0, padx=4, pady=6, sticky='e') Label(frame_event, text=_('Category')).grid(row=6, column=0, padx=4, pady=6, sticky='e') Button(frame_event, image=self.img_bell, command=self.add_reminder, padding=0).grid(row=7, column=0, padx=4, pady=6, sticky='en') self.summary = Entry(frame_event, width=35) self.summary.insert(0, self.event['Summary']) self.summary.grid(row=0, column=1, padx=4, pady=6, sticky='ew') self.place = Entry(frame_event, width=35) self.place.insert(0, self.event['Place']) self.place.grid(row=1, column=1, padx=4, pady=6, sticky='ew') frame_start = Frame(frame_event) frame_start.grid(row=2, column=1, padx=4, pady=6, sticky='w') frame_end = Frame(frame_event) frame_end.grid(row=3, column=1, padx=4, pady=6, sticky='w') txt_frame = Frame(frame_event, style='txt.TFrame', border=1, relief='sunken') self.desc = Text(txt_frame, width=35, height=4, highlightthickness=0, relief='flat', selectbackground=active_bg) self.desc.insert('1.0', self.event['Description']) self.desc.pack(fill='both', expand=True) txt_frame.grid(row=5, column=1, padx=4, pady=6, sticky='ewsn') cats = list(CONFIG.options('Categories')) width = max([len(cat) for cat in cats]) self.category = Combobox(frame_event, width=width + 2, values=cats, state='readonly') self.category.set(event['Category']) self.category.grid(row=6, column=1, padx=4, pady=6, sticky='w') self.frame_alarms = Frame(frame_event) self.frame_alarms.grid(row=7, column=1, sticky='w') # --- *--- task Checkbutton(frame_task, text=_('Task'), command=self._change_label, variable=self._task).pack(side='left') self.task_progress = Combobox(frame_task, state='readonly', width=9, values=(_('Pending'), _('In Progress'), _('Completed'), _('Cancelled'))) self.task_progress.pack(side='left', padx=(8, 4)) self.in_progress = Combobox( frame_task, state='readonly', width=5, values=['{}%'.format(i) for i in range(0, 110, 10)]) self.in_progress.pack(side='left', padx=4) if not event['Task']: self.task_progress.set(_('Pending')) self.in_progress.set('0%') elif '%' in event['Task']: self.task_progress.set(_('In Progress')) self.in_progress.set(event['Task']) else: self.task_progress.set(_(event['Task'])) self.in_progress.set('0%') # calendar settings prop = { op: CONFIG.get('Calendar', op) for op in CONFIG.options('Calendar') } prop['font'] = "Liberation\ Sans 9" prop.update(selectforeground='white', selectbackground=active_bg) locale = CONFIG.get('General', 'locale') # --- *--- start date self.start_date = self.event['Start'] self.start_entry = DateEntry(frame_start, locale=locale, width=10, justify='center', year=self.start_date.year, month=self.start_date.month, day=self.start_date.day, **prop) self.start_hour = Combobox(frame_start, width=3, justify='center', state='readonly', exportselection=False, values=['%02d' % i for i in range(24)]) self.start_hour.set('%02d' % self.start_date.hour) self.start_min = Combobox(frame_start, width=3, justify='center', state='readonly', exportselection=False, values=['%02d' % i for i in range(0, 60, 5)]) self.start_min.set('%02d' % self.start_date.minute) self.start_entry.pack(side='left', padx=(0, 18)) self.start_hour.pack(side='left', padx=(4, 0)) self.start_date = self.start_date.date() Label(frame_start, text=':').pack(side='left') self.start_min.pack(side='left', padx=(0, 4)) Checkbutton(frame_start, text=_("whole day"), variable=self._whole_day, command=self._toggle_whole_day).pack(side='left', padx=4) # --- *--- end date self.end_date = self.event['End'] self.end_entry = DateEntry(frame_end, justify='center', locale=locale, width=10, year=self.end_date.year, month=self.end_date.month, day=self.end_date.day, **prop) self.end_hour = Combobox(frame_end, width=3, justify='center', state='readonly', exportselection=False, values=['%02d' % i for i in range(24)]) self.end_hour.set('%02d' % self.end_date.hour) self.end_min = Combobox(frame_end, width=3, justify='center', state='readonly', exportselection=False, values=['%02d' % i for i in range(0, 60, 5)]) self.end_min.set('%02d' % self.end_date.minute) self.end_entry.pack(side='left', padx=(0, 18)) self.end_hour.pack(side='left', padx=(4, 0)) Label(frame_end, text=':').pack(side='left') self.end_min.pack(side='left', padx=(0, 4)) self.end_date = self.end_date.date() for date in self.event['Reminders'].values(): self.add_reminder(date) self._toggle_whole_day() # --- repetition settings frame_rep = Frame(notebook) notebook.add(frame_rep, text=_('Repetition'), padding=4, sticky='eswn') frame_rep.columnconfigure(0, weight=1) frame_rep.columnconfigure(1, weight=1) frame_rep.rowconfigure(1, weight=1) self._repeat = BooleanVar(self, bool(self.event['Repeat'])) repeat = { 'Frequency': 'year', 'Limit': 'always', 'NbTimes': 1, 'EndDate': (datetime.now() + timedelta(days=1)).date(), 'WeekDays': [self.start_date.isocalendar()[2] - 1] } repeat.update(self.event['Repeat']) self._repeat_freq = StringVar(self, repeat['Frequency']) Checkbutton(frame_rep, text=_('Repeat event'), variable=self._repeat, command=self._toggle_rep).grid(row=0, column=0, columnspan=2, padx=4, pady=6, sticky='w') # --- *--- Frequency frame_freq = LabelFrame(frame_rep, text=_('Frequency')) frame_freq.grid(row=1, column=0, sticky='eswn', padx=(0, 3)) self._lfreq = Label(frame_freq, text=_('Every:')) self._lfreq.grid(row=0, column=0, padx=4, pady=2, sticky='e') self._freqs = [] for i, val in enumerate(['Year', 'Month', 'Week']): r = Radiobutton(frame_freq, text=_(val), variable=self._repeat_freq, value=val.lower(), command=self._toggle_wd) r.grid(row=i, column=1, padx=4, pady=2, sticky='nw') self._freqs.append(r) frame_days = Frame(frame_freq) frame_days.grid(row=2, column=2, padx=4, pady=2, sticky='nw') self._week_days = [] days = get_day_names("wide", locale=locale) days = [days[i] for i in range(7)] for day in days: ch = Checkbutton(frame_days, text=day) ch.pack(anchor='w') self._week_days.append(ch) for d in repeat['WeekDays']: self._week_days[int(d)].state(('selected', )) # --- *--- Limit frame_lim = LabelFrame(frame_rep, text=_('Limit')) frame_lim.grid(row=1, column=1, sticky='eswn', padx=(3, 0)) frame_lim.grid(row=1, column=1, sticky='eswn', padx=(3, 0)) self._repeat_lim = StringVar(self, repeat['Limit']) # always r1 = Radiobutton(frame_lim, text=_('Always'), value='always', variable=self._repeat_lim, command=self._toggle_lim) r1.grid(row=0, column=0, sticky='w') # until r2 = Radiobutton(frame_lim, text=_('Until'), value='until', variable=self._repeat_lim, command=self._toggle_lim) r2.grid(row=1, column=0, sticky='w') until_date = repeat['EndDate'] self.until_entry = DateEntry(frame_lim, width=10, justify='center', locale=locale, year=until_date.year, month=until_date.month, day=until_date.day, **prop) self.until_entry.grid(row=1, column=1, columnspan=2, sticky='w', padx=(4, 10), pady=2) # after r3 = Radiobutton(frame_lim, text=_('After'), value='after', variable=self._repeat_lim, command=self._toggle_lim) r3.grid(row=2, column=0, sticky='w') frame_after = Frame(frame_lim, style='txt.TFrame', relief='sunken', border=1) self.s_after = Spinbox(frame_after, from_=0, to=100, width=3, justify='center', relief='flat', highlightthickness=0, validate='key', validatecommand=(self._only_nb, '%P'), disabledbackground='white') self.s_after.pack() self.s_after.delete(0, 'end') self.s_after.insert(0, str(repeat['NbTimes'])) frame_after.grid(row=2, column=1, padx=4, pady=2, sticky='w') self._llim = Label(frame_lim, text=_('times')) self._llim.grid(row=2, column=2, padx=0, pady=2, sticky='w') self._rb_lim = [r1, r2, r3] self._toggle_rep() self._change_label() # --- bindings self.bind('<Configure>') self.task_progress.bind('<<ComboboxSelected>>', self._toggle_in_progress) self.start_entry.bind('<<DateEntrySelected>>', self._select_start) self.end_entry.bind('<<DateEntrySelected>>', self._select_end) self.start_hour.bind("<<ComboboxSelected>>", self._select_start_hour) self.start_min.bind("<<ComboboxSelected>>", self._select_start_min) self.end_min.bind("<<ComboboxSelected>>", self._select_end_time) self.end_hour.bind("<<ComboboxSelected>>", self._select_end_time) self.bind_class("TCombobox", "<<ComboboxSelected>>", self.__clear_selection, add=True) # self.wait_visibility(self) # self.grab_set() self.summary.focus_set()
class Params(Toplevel): def __init__(self, parent, **options): """ créer le Toplevel permettant de modifier les paramètres """ Toplevel.__init__(self, parent, **options) self.grab_set() self.transient(parent) self.title(_("Settings")) self.resizable(0, 0) self.onglets = Notebook(self) self.onglets.grid(row=0,column=0,columnspan=2) self.im_color = PhotoImage(master=self, file=COLOR) self.style = Style(self) self.style.theme_use(STYLE) self.style.configure('title.TLabel', font='CMU\ Sans\ Serif\ Demi\ Condensed 12') self.okfct = self.register(valide_entree_nb) self.nb_task = len(CONFIG.options("Tasks")) # Général (temps, police et langue) self.general = Frame(self.onglets, padding=10) self.general.pack(fill="both", expand=True, padx=10, pady=10) self.onglets.add(self.general, text=_("General")) # Temps Label(self.general, text=_("Times:"), style='title.TLabel').grid(row=0, pady=4, sticky="w") self.time_frame = Frame(self.general) self.time_frame.grid(row=1, sticky="ew") Label(self.time_frame, text=_("Work: ")).grid(row=0, column=0) self.travail = Entry(self.time_frame, width=4, justify='center', validatecommand=(self.okfct, '%d', '%S'), validate='key') self.travail.insert(0, CONFIG.get("Work", "time")) self.travail.grid(row=0, column=1, padx=(0,10)) Label(self.time_frame, text=_("Break: ")).grid(row=0, column=2) self.pause = Entry(self.time_frame, width=4, justify='center', validatecommand=(self.okfct, '%d', '%S'), validate='key') self.pause.insert(0, CONFIG.get("Break", "time")) self.pause.grid(row=0, column=3, padx=(0,10)) Label(self.time_frame, text=_("Rest: ")).grid(row=0, column=4) self.rest = Entry(self.time_frame, width=4, justify='center', validatecommand=(self.okfct, '%d', '%S'), validate='key') self.rest.insert(0, CONFIG.get("Rest", "time")) self.rest.grid(row=0, column=5) Separator(self.general, orient='horizontal').grid(row=2, sticky="ew", pady=10) # Police self.font_frame = Frame(self.general) self.font_frame.grid(row=3, pady=4, sticky="ew") Label(self.font_frame, text=_("Font:"), style='title.TLabel').pack(anchor="n", side="left") self.exemple = Label(self.font_frame, text="02:17", anchor="center", font="%s %i" % (CONFIG.get("General", "font"), CONFIG.getint("General", "fontsize")), relief="groove") self.exemple.pack(side="right") self.font_frame2 = Frame(self.general) self.font_frame2.grid(row=4) Label(self.font_frame2, text=_("Family: ")).grid(row=0, column=0, sticky="e") self.font = Entry(self.font_frame2, justify='center') self.font.insert(0, CONFIG.get("General", "font")) self.font.grid(row=0, column=1, padx=(0,10), sticky="ew") self.font.bind('<FocusOut>', self.actualise_police) self.font.bind('<Key-Return>', self.actualise_police, True) Label(self.font_frame2, text=_("Size: ")).grid(row=0, column=2, sticky="e") self.size = Entry(self.font_frame2, width=4, justify='center', validatecommand=(self.okfct, '%d', '%S'), validate='key') self.size.insert(0, CONFIG.getint("General", "fontsize")) self.size.grid(row=0, column=3, pady=2, sticky="w") self.size.bind('<FocusOut>', self.actualise_police) self.size.bind('<Key-Return>', self.actualise_police, True) Separator(self.general, orient='horizontal').grid(row=5, sticky="ew", pady=10) # Langues self.lang_frame = Frame(self.general) self.lang_frame.grid(row=6, pady=4, sticky="ew") Label(self.lang_frame, text=_("Language:"), style='title.TLabel').pack(side="left") self.lang = StringVar(self.lang_frame, LANGUES[CONFIG.get("General", "language")]) b_lang = Menubutton(self.lang_frame, textvariable=self.lang) menu = Menu(b_lang, tearoff=False) menu.add_radiobutton(label="English", variable=self.lang, value="English", command=self.change_langue) menu.add_radiobutton(label="Français", variable=self.lang, value="Français", command=self.change_langue) b_lang.configure(menu=menu) b_lang.pack(side="right") # Son self.im_son = PhotoImage(master=self, file=SON) self.im_mute = PhotoImage(master=self, file=MUTE) self.son = Frame(self.onglets, padding=10) self.son.pack(fill="both", expand=True, padx=10, pady=10) self.son.columnconfigure(1, weight=1) self.onglets.add(self.son, text=_("Sound")) Label(self.son, text=_("Sound:"), style='title.TLabel').grid(row=0, pady=4, sticky="w") self.mute = BooleanVar(self) self.mute.set(not CONFIG.get("Sound", "mute")) def mute_unmute(): if self.mute.get(): self.mute.set(False) b_son.configure(image=self.im_son) else: self.mute.set(True) b_son.configure(image=self.im_mute) b_son = Button(self.son, command=mute_unmute) mute_unmute() b_son.grid(row=0, column=1, sticky="e", pady=4) self.son_frame = Frame(self.son) self.son_frame.grid(row=1, sticky="ew", columnspan=2) self.bip = Entry(self.son_frame, justify='center') self.bip.insert(0, CONFIG.get("Sound", "beep")) self.bip.pack(side="left", fill="both", expand=True) Button(self.son_frame, text="...", width=2, command=self.choix_son).pack(side="right", padx=(2,0)) if PL[0] != "w": Separator(self.son, orient='horizontal').grid(row=2, columnspan=2, sticky="ew", pady=10) son_frame2 = Frame(self.son) son_frame2.grid(row=3, sticky="ew", columnspan=2) Label(son_frame2, text=_("Audio player: "), style='title.TLabel').pack(side="left") self.player = Entry(son_frame2, justify='center') self.player.insert(0, CONFIG.get("Sound", "player")) self.player.pack(side="right", fill="both", expand=True) # Couleurs self.couleurs = Frame(self.onglets, padding=10) self.couleurs.pack(fill="both", expand=True, padx=10, pady=10) self.onglets.add(self.couleurs, text=_("Colors")) # style des boutons de choix des couleurs self.style.configure("fond_w.TButton", background=CONFIG.get("Work", "bg")) self.style.configure("fond_p.TButton", background=CONFIG.get("Break", "bg")) self.style.configure("fond_r.TButton", background=CONFIG.get("Rest", "bg")) self.style.configure("texte_w.TButton", background=CONFIG.get("Work", "fg")) self.style.configure("texte_p.TButton", background=CONFIG.get("Break", "fg")) self.style.configure("texte_r.TButton", background=CONFIG.get("Rest", "fg")) self.couleurs.grid_columnconfigure(3, weight=3) self.couleurs.grid_rowconfigure(0, weight=1) Label(self.couleurs, text=_("Work: "), style='title.TLabel').grid(row=0, column=0, pady=4, padx=(2,10), sticky="w") Label(self.couleurs, text=_("Background: ")).grid(row=0, column=1, sticky="e", pady=(6,4)) Button(self.couleurs, width=2, command=lambda: self.choix_couleur("fond_w"), style='fond_w.TButton').grid(row=0, column=2, pady=4) Label(self.couleurs, text=_("Text: ")).grid(row=1, column=1, sticky="e") Button(self.couleurs, width=2, command=lambda: self.choix_couleur("texte_w"), style='texte_w.TButton').grid(row=1, column=2, pady=4) Separator(self.couleurs, orient='horizontal').grid(row=2, sticky="ew", pady=10, columnspan=4) Label(self.couleurs, text=_("Break: "), style='title.TLabel').grid(row=3, column=0, pady=4, padx=(2,10), sticky="w") Label(self.couleurs, text=_("Background: ")).grid(row=3, column=1, sticky="e", pady=(6,4)) Button(self.couleurs, width=2, command=lambda: self.choix_couleur("fond_p"), style='fond_p.TButton').grid(row=3, column=2, pady=4) Label(self.couleurs, text=_("Text: ")).grid(row=4, column=1, sticky="e") Button(self.couleurs, width=2, command=lambda: self.choix_couleur("texte_p"), style='texte_p.TButton').grid(row=4, column=2, pady=4) Separator(self.couleurs, orient='horizontal').grid(row=5, sticky="ew", pady=10, columnspan=4) Label(self.couleurs, text=_("Rest: "), style='title.TLabel').grid(row=6, column=0, pady=4, sticky="w", padx=(2,10)) Label(self.couleurs, text=_("Background: ")).grid(row=6, column=1, sticky="e", pady=(6,4)) Button(self.couleurs, width=2, command=lambda: self.choix_couleur("fond_r"), style='fond_r.TButton').grid(row=6, column=2, pady=4) Label(self.couleurs, text=_("Text: ")).grid(row=7, column=1, sticky="e") Button(self.couleurs, width=2, command=lambda: self.choix_couleur("texte_r"), style='texte_r.TButton').grid(row=7, column=2, pady=4) # Stats self.stats = Frame(self.onglets, padding=10) self.stats.pack(fill="both", expand=True, padx=10, pady=10) self.stats.grid_columnconfigure(2, weight=1) self.onglets.add(self.stats, text=_("Statistics")) Label(self.stats, text=_("Statistics:"), style='title.TLabel').grid(row=0, column=0, pady=4, sticky="w") tasks = [t.capitalize() for t in CONFIG.options("Tasks")] cmap = [CONFIG.get("Tasks", task) for task in tasks] for i, coul, task in zip(range(self.nb_task), cmap , tasks): Label(self.stats, text=task).grid(row=i + 1, column=0, sticky="e", padx=4, pady=4) self.style.configure("t%i.TButton" % i, background=coul) Button(self.stats, style="t%i.TButton" % i, width=2, command=lambda j=i: self.coul_stat(j)).grid(row=i + 1, column=1, pady=4) # Validation Button(self, text="Ok", command=self.valide).grid(row=1,column=1, sticky="we") Button(self, text=_("Cancel"), command=self.destroy).grid(row=1,column=0, sticky="we") def actualise_police(self, event): """ actualise le texte d'exemple de la police choisie """ family = self.font.get() family = "\ ".join(family.split(" ")) self.exemple.configure(font="%s %s" % (family, self.size.get())) def choix_couleur(self, type_mode): """ sélection de la couleur du fond/texte pour chaque mode (travail/pause/repos) """ coul = askcolor(self.style.lookup(type_mode+".TButton", 'background')) if coul: self.style.configure(type_mode+".TButton", background=coul[1]) def coul_stat(self, i): """ choix des couleurs pour l'affichage des stats """ coul = askcolor(self.style.lookup("t%i.TButton" % i, "background")) if coul: self.style.configure("t%i.TButton" % i, background=coul[1]) def valide(self): old_tpsw = CONFIG.getint("Work", "time") old_tpsp = CONFIG.getint("Break", "time") old_tpsr = CONFIG.getint("Rest", "time") tpsw = int(self.travail.get()) tpsp = int(self.pause.get()) tpsr = int(self.rest.get()) pausefg = self.style.lookup("texte_p.TButton", "background") pausebg = self.style.lookup("fond_p.TButton", "background") workfg = self.style.lookup("texte_w.TButton", "background") workbg = self.style.lookup("fond_w.TButton", "background") restfg = self.style.lookup("texte_r.TButton", "background") restbg =self.style.lookup("fond_r.TButton", "background") son = self.bip.get() if PL[0] != "w": player = self.player.get() else: player = CONFIG.get("Sound", "player") mute = self.mute.get() family = self.font.get() family = "\ ".join(family.split(" ")) size = self.size.get() cmap = [] for i in range(self.nb_task): cmap.append(self.style.lookup("t%i.TButton" % i, "background")) if PL[0] == "w": filetypes = ['wav'] else: filetypes = ["ogg", "wav", "mp3"] if (tpsw > 0 and tpsp > 0 and tpsr > 0 and os.path.exists(son) and (son.split('.')[-1] in filetypes)): CONFIG.set("General", "language", self.lang.get()[:2].lower()) CONFIG.set("General", "font", family) CONFIG.set("General", "fontsize", size) CONFIG.set("Work", "time", str(tpsw)) CONFIG.set("Work", "bg", workbg) CONFIG.set("Work", "fg", workfg) CONFIG.set("Break", "time", str(tpsp)) CONFIG.set("Break", "bg", pausebg) CONFIG.set("Break", "fg", pausefg) CONFIG.set("Rest", "time", str(tpsr)) CONFIG.set("Rest", "bg", restbg) CONFIG.set("Rest", "fg", restfg) CONFIG.set("Sound", "beep", son) CONFIG.set("Sound", "player", player) CONFIG.set("Sound", "mute", str(mute)) for task, col in zip(CONFIG.options("Tasks"), cmap): CONFIG.set("Tasks", task, col) self.master.set_config() with open(PATH_CONFIG, "w") as file: CONFIG.write(file) if (old_tpsw != CONFIG.getint("Work", "time") or old_tpsp != CONFIG.getint("Break", "time") or old_tpsr != CONFIG.getint("Rest", "time")): self.master.stop(False) self.destroy() else: showerror(_("Error"),_("There is at least one invalid setting!")) def change_langue(self): showinfo(_("Information"), _("The language setting will take effect after restarting the application.")) def choix_son(self): if PL[0] == "w": filetypes = [('WAV','*.wav')] else: filetypes = [(_("sound file"), '*.mp3 *.ogg *.wav'), ('OGG', '*.ogg'), ('MP3', '*.mp3'), ('WAV','*.wav')] init = self.bip.get() if not os.path.exists(init): init=CONFIG.get("Sound", "beep") fich = askopenfilename(filetypes=filetypes, initialfile=os.path.split(init)[1], initialdir=os.path.dirname(init)) if fich: self.bip.delete(0,"end") self.bip.insert(0, fich)
def __init__(self, master, font_dict={}, text="Abcd", title="Font Chooser", **kwargs): """ Create a new FontChooser instance. Arguments: master : Tk or Toplevel instance master window font_dict : dict dictionnary, like the one returned by the ``actual`` method of a ``Font`` object: :: {'family': str, 'size': int, 'weight': 'bold'/'normal', 'slant': 'italic'/'roman', 'underline': bool, 'overstrike': bool} text : str text to be displayed in the preview label title : str window title kwargs : dict additional keyword arguments to be passed to ``Toplevel.__init__`` """ Toplevel.__init__(self, master, **kwargs) self.title(title) self.resizable(False, False) self.protocol("WM_DELETE_WINDOW", self.quit) self._validate_family = self.register(self.validate_font_family) self._validate_size = self.register(self.validate_font_size) # --- variable storing the chosen font self.res = "" style = Style(self) style.configure("prev.TLabel", background="white") bg = style.lookup("TLabel", "background") self.configure(bg=bg) # --- family list self.fonts = list(set(families())) self.fonts.append("TkDefaultFont") self.fonts.sort() for i in range(len(self.fonts)): self.fonts[i] = self.fonts[i].replace(" ", "\ ") max_length = int(2.5 * max([len(font) for font in self.fonts])) // 3 self.sizes = ["%i" % i for i in (list(range(6, 17)) + list(range(18, 32, 2)))] # --- font default font_dict["weight"] = font_dict.get("weight", "normal") font_dict["slant"] = font_dict.get("slant", "roman") font_dict["underline"] = font_dict.get("underline", False) font_dict["overstrike"] = font_dict.get("overstrike", False) font_dict["family"] = font_dict.get("family", self.fonts[0].replace('\ ', ' ')) font_dict["size"] = font_dict.get("size", 10) # --- creation of the widgets # ------ style parameters (bold, italic ...) options_frame = Frame(self, relief='groove', borderwidth=2) self.font_family = StringVar(self, " ".join(self.fonts)) self.font_size = StringVar(self, " ".join(self.sizes)) self.var_bold = BooleanVar(self, font_dict["weight"] == "bold") b_bold = Checkbutton(options_frame, text=TR["Bold"], command=self.toggle_bold, variable=self.var_bold) b_bold.grid(row=0, sticky="w", padx=4, pady=(4, 2)) self.var_italic = BooleanVar(self, font_dict["slant"] == "italic") b_italic = Checkbutton(options_frame, text=TR["Italic"], command=self.toggle_italic, variable=self.var_italic) b_italic.grid(row=1, sticky="w", padx=4, pady=2) self.var_underline = BooleanVar(self, font_dict["underline"]) b_underline = Checkbutton(options_frame, text=TR["Underline"], command=self.toggle_underline, variable=self.var_underline) b_underline.grid(row=2, sticky="w", padx=4, pady=2) self.var_overstrike = BooleanVar(self, font_dict["overstrike"]) b_overstrike = Checkbutton(options_frame, text=TR["Overstrike"], variable=self.var_overstrike, command=self.toggle_overstrike) b_overstrike.grid(row=3, sticky="w", padx=4, pady=(2, 4)) # ------ Size and family self.var_size = StringVar(self) self.entry_family = Entry(self, width=max_length, validate="key", validatecommand=(self._validate_family, "%d", "%S", "%i", "%s", "%V")) self.entry_size = Entry(self, width=4, validate="key", textvariable=self.var_size, validatecommand=(self._validate_size, "%d", "%P", "%V")) self.list_family = Listbox(self, selectmode="browse", listvariable=self.font_family, highlightthickness=0, exportselection=False, width=max_length) self.list_size = Listbox(self, selectmode="browse", listvariable=self.font_size, highlightthickness=0, exportselection=False, width=4) scroll_family = Scrollbar(self, orient='vertical', command=self.list_family.yview) scroll_size = Scrollbar(self, orient='vertical', command=self.list_size.yview) self.preview_font = Font(self, **font_dict) if len(text) > 30: text = text[:30] self.preview = Label(self, relief="groove", style="prev.TLabel", text=text, font=self.preview_font, anchor="center") # --- widget configuration self.list_family.configure(yscrollcommand=scroll_family.set) self.list_size.configure(yscrollcommand=scroll_size.set) self.entry_family.insert(0, font_dict["family"]) self.entry_family.selection_clear() self.entry_family.icursor("end") self.entry_size.insert(0, font_dict["size"]) try: i = self.fonts.index(self.entry_family.get().replace(" ", "\ ")) except ValueError: # unknown font i = 0 self.list_family.selection_clear(0, "end") self.list_family.selection_set(i) self.list_family.see(i) try: i = self.sizes.index(self.entry_size.get()) self.list_size.selection_clear(0, "end") self.list_size.selection_set(i) self.list_size.see(i) except ValueError: # size not in list pass self.entry_family.grid(row=0, column=0, sticky="ew", pady=(10, 1), padx=(10, 0)) self.entry_size.grid(row=0, column=2, sticky="ew", pady=(10, 1), padx=(10, 0)) self.list_family.grid(row=1, column=0, sticky="nsew", pady=(1, 10), padx=(10, 0)) self.list_size.grid(row=1, column=2, sticky="nsew", pady=(1, 10), padx=(10, 0)) scroll_family.grid(row=1, column=1, sticky='ns', pady=(1, 10)) scroll_size.grid(row=1, column=3, sticky='ns', pady=(1, 10)) options_frame.grid(row=0, column=4, rowspan=2, padx=10, pady=10, ipadx=10) self.preview.grid(row=2, column=0, columnspan=5, sticky="eswn", padx=10, pady=(0, 10), ipadx=4, ipady=4) button_frame = Frame(self) button_frame.grid(row=3, column=0, columnspan=5, pady=(0, 10), padx=10) Button(button_frame, text="Ok", command=self.ok).grid(row=0, column=0, padx=4, sticky='ew') Button(button_frame, text=TR["Cancel"], command=self.quit).grid(row=0, column=1, padx=4, sticky='ew') self.list_family.bind('<<ListboxSelect>>', self.update_entry_family) self.list_size.bind('<<ListboxSelect>>', self.update_entry_size, add=True) self.list_family.bind("<KeyPress>", self.keypress) self.entry_family.bind("<Return>", self.change_font_family) self.entry_family.bind("<Tab>", self.tab) self.entry_size.bind("<Return>", self.change_font_size) self.entry_family.bind("<Down>", self.down_family) self.entry_size.bind("<Down>", self.down_size) self.entry_family.bind("<Up>", self.up_family) self.entry_size.bind("<Up>", self.up_size) # bind Ctrl+A to select all instead of go to beginning self.bind_class("TEntry", "<Control-a>", self.select_all) self.wait_visibility(self) self.grab_set() self.entry_family.focus_set() self.lift()
def __init__(self, master, font_dict=None, text='AaBbYyZz', title='Font', **kwargs): Toplevel.__init__(self, master, **kwargs) self.title(title) try: self.wm_iconbitmap('transparent.ico') except TclError: pass self.resizable(False, False) self.protocol('WM_DELETE_WINDOW', self.quit) self._validate_family = self.register(self.validate_font_family) self._validate_size = self.register(self.validate_font_size) # --- variable storing the chosen font self.res = '' style = Style(self) style.configure('prev.TLabel') bg = style.lookup('TLabel', 'background') self.configure(bg=bg) # --- family list self.fonts = list(set(families())) self.fonts.append('TkDefaultFont') self.fonts.sort() for i in range(len(self.fonts)): self.fonts[i] = self.fonts[i].replace(' ', '\\ ') max_length = int(2.5 * max([len(font) for font in self.fonts])) // 3 - 2 self.sizes = ['%i' % i for i in (list(range(6, 17)) + list(range(18, 32, 2)) + list(range(36, 48, 4)))] # --- font default font_dict['weight'] = font_dict.get('weight', 'normal') font_dict['slant'] = font_dict.get('slant', 'roman') font_dict['underline'] = font_dict.get('underline', False) font_dict['overstrike'] = font_dict.get('overstrike', False) font_dict['family'] = font_dict.get('family', self.fonts[0].replace('\\ ', ' ')) font_dict['size'] = font_dict.get('size', 10) # --- format list self.formats = ['Regular', 'Italic', 'Bold', 'Bold Italic'] # --- creation of the widgets self.font_family = StringVar(self, ' '.join(self.fonts)) self.font_size = StringVar(self, ' '.join(self.sizes)) self.format_type = StringVar(self, self.formats) self.var_bold = BooleanVar(self, font_dict['weight'] == 'bold') self.var_italic = BooleanVar(self, font_dict['slant'] == 'italic') # ------ Size and family self.var_size = StringVar(self) self.entry_family = Entry(self, width=max_length, validate='key', validatecommand=(self._validate_family, '%d', '%S', '%i', '%s', '%V')) self.entry_size = Entry(self, width=8, validate='key', textvariable=self.var_size, validatecommand=(self._validate_size, '%d', '%P', '%V')) self.entry_format = Entry(self) self.list_family = Listbox(self, selectmode='browse', listvariable=self.font_family, highlightthickness=0, exportselection=False, width=max_length, height=6) self.list_size = Listbox(self, selectmode='browse', listvariable=self.font_size, highlightthickness=0, exportselection=False, width=6, height=6) self.list_format = Listbox(self, selectmode='browse', listvariable=self.format_type, highlightthickness=0, exportselection=False, width=12, height=6) self.scroll_family = Scrollbar(self, orient='vertical', command=self.list_family.yview) self.scroll_size = Scrollbar(self, orient='vertical', command=self.list_size.yview) self.scroll_format = Scrollbar(self, orient='vertical', command=self.list_format.yview) self.family_label = Label(self, text='Font:') self.style_label = Label(self, text='Font style:') self.size_label = Label(self, text='Size:') self.script_label = Label(self, text='Script:') self.script_box = Combobox(self, values=['Western']) self.more_fonts_label = Lbl(self, text='Show more fonts', underline=1, fg='blue', cursor='hand2') f = Font(self.more_fonts_label, self.more_fonts_label.cget("font")) f.configure(underline=True) self.more_fonts_label.configure(font=f) self.preview_font = Font(self, **font_dict) if len(text) > 30: text = text[:30] self.preview_window = LabelFrame(self, relief='groove', text='Sample', bd=1) # --- widget configuration self.list_family.configure(yscrollcommand=self.scroll_family.set) self.list_size.configure(yscrollcommand=self.scroll_size.set) self.list_format.configure(yscrollcommand=self.scroll_format.set) self.entry_family.insert(0, font_dict['family']) self.entry_family.selection_clear() self.entry_family.icursor('end') self.entry_format.insert(0, self.formats[1]) self.entry_format.selection_clear() self.entry_format.icursor('end') self.entry_size.insert(0, font_dict['size']) try: i = self.fonts.index(self.entry_family.get().replace(' ', '\\ ')) except ValueError: # unknown font i = 0 self.list_family.selection_clear(0, 'end') self.list_family.selection_set(i) self.list_family.see(i) try: i = self.sizes.index(self.entry_size.get()) self.list_size.selection_clear(0, 'end') self.list_size.selection_set(i) self.list_size.see(i) except ValueError: # size not in listtsg pass # font family location config self.family_label.grid(row=0, column=0, sticky='nsew', pady=(10, 1), padx=(10, 1)) self.entry_family.grid(row=1, column=0, sticky='nsew', pady=(1, 1), padx=(10, 0), columnspan=2) self.list_family.grid(row=2, column=0, sticky='nsew', pady=(1, 10), padx=(10, 0)) self.scroll_family.grid(row=2, column=1, sticky='ns', pady=(1, 10)) # font style/format location config self.style_label.grid(row=0, column=2, sticky='nsew', pady=(10, 1), padx=(15, 1)) self.entry_format.grid(row=1, column=2, sticky='nsew', pady=(1, 1), padx=(15, 0), columnspan=2) self.list_format.grid(row=2, column=2, sticky='nsew', pady=(1, 10), padx=(15, 0)) self.scroll_format.grid(row=2, column=3, sticky='ns', pady=(1, 10)) # font size location config self.size_label.grid(row=0, column=4, sticky='nsew', pady=(10, 1), padx=(15, 1)) self.entry_size.grid(row=1, column=4, sticky='nsew', pady=(1, 1), padx=(15, 10), columnspan=2) self.list_size.grid(row=2, column=4, sticky='nsew', pady=(1, 10), padx=(15, 0)) self.scroll_size.grid(row=2, column=5, sticky='nsew', pady=(1, 10), padx=(0, 10)) # font preview location config self.preview_window.grid(row=4, column=2, columnspan=4, sticky='nsew', rowspan=2, padx=15, pady=(0, 10), ipadx=10, ipady=10) self.preview_window.config(height=75) preview = Label(self.preview_window, text=text, font=self.preview_font, anchor='center') preview.place(relx=0.5, rely=0.5, anchor='center') self.script_label.grid(row=6, column=2, sticky='nsw', padx=(15, 0)) self.script_box.grid(row=7, column=2, sticky='nsw', pady=(1, 30), padx=(15, 0)) self.script_box.current(0) self.more_fonts_label.grid(row=8, column=0, pady=(35, 20), padx=(15, 0), sticky='nsw') button_frame = Frame(self) button_frame.grid(row=9, column=2, columnspan=4, pady=(0, 10), padx=(10, 0)) Button(button_frame, text='Ok', command=self.ok).grid(row=0, column=0, padx=4, sticky='ew') Button(button_frame, text='Cancel', command=self.quit).grid(row=0, column=1, padx=4, sticky='ew') self.list_family.bind('<<ListboxSelect>>', self.update_entry_family) self.list_format.bind('<<ListboxSelect>>', self.update_entry_format) self.list_size.bind('<<ListboxSelect>>', self.update_entry_size, add=True) self.list_family.bind('<KeyPress>', self.keypress) self.entry_family.bind('<Return>', self.change_font_family) self.entry_family.bind('<Tab>', self.tab) self.entry_size.bind('<Return>', self.change_font_size) self.more_fonts_label.bind('<Button-1>', search_fonts) self.entry_family.bind('<Down>', self.down_family) self.entry_size.bind('<Down>', self.down_size) self.entry_family.bind('<Up>', self.up_family) self.entry_size.bind('<Up>', self.up_size) self.bind_class('TEntry', '<Control-a>', self.select_all) self.wait_visibility(self) self.grab_set() self.entry_family.focus_set() self.lift()
class ToggledFrame(Frame): """ A frame that can be toggled to open and close """ def __init__(self, master=None, text="", **kwargs): font = kwargs.pop('font', '') Frame.__init__(self, master, **kwargs) self.style_name = self.cget('style') self.toggle_style_name = '%s.Toggle' % ('.'.join( self.style_name.split('.')[:-1])) self.columnconfigure(1, weight=1) self.rowconfigure(1, weight=1) self.style = Style(self) self.style.configure(self.toggle_style_name, background=self.style.lookup( self.style_name, 'background')) self.style.map(self.toggle_style_name, background=[]) self._checkbutton = Checkbutton(self, style=self.toggle_style_name, command=self.toggle, cursor='arrow') self.label = Label(self, text=text, font=font, style=self.style_name.replace('TFrame', 'TLabel')) self.interior = Frame(self, style=self.style_name) self.interior.grid(row=1, column=1, sticky="nswe", padx=(4, 0)) self.interior.grid_remove() self.label.bind('<Configure>', self._wrap) self.label.bind('<1>', lambda e: self._checkbutton.invoke()) self._grid_widgets() self.bind('<<ThemeChanged>>', self._theme_changed) def _theme_changed(self, event): self.style.configure(self.toggle_style_name, background=self.style.lookup( self.style_name, 'background')) def _wrap(self, event): self.label.configure(wraplength=self.label.winfo_width()) def _grid_widgets(self): self._checkbutton.grid(row=0, column=0) self.label.grid(row=0, column=1, sticky="we") def toggle(self): if 'selected' not in self._checkbutton.state(): self.interior.grid_remove() self.event_generate("<<ToggledFrameClose>>") else: self.interior.grid() self.event_generate("<<ToggledFrameOpen>>") def open(self): self._checkbutton.state(('selected', )) self.interior.grid() self.event_generate("<<ToggledFrameOpen>>") def close(self): self._checkbutton.state(('!selected', )) self.interior.grid_remove() self.event_generate("<<ToggledFrameClose>>")
class Sticky(Toplevel): """ Sticky note class """ def __init__(self, master, key, **kwargs): """ Create a new sticky note. master: main app key: key identifying this note in master.note_data kwargs: dictionnary of the other arguments (title, txt, category, color, tags, geometry, locked, checkboxes, images, rolled) """ Toplevel.__init__(self, master) # --- window properties self.id = key self.is_locked = not (kwargs.get("locked", False)) self.images = [] self.links = {} self.latex = {} self.nb_links = 0 self.title('mynotes%s' % key) self.attributes("-type", "splash") self.attributes("-alpha", CONFIG.getint("General", "opacity") / 100) self.focus_force() # window geometry self.update_idletasks() self.geometry(kwargs.get("geometry", '220x235')) self.save_geometry = kwargs.get("geometry", '220x235') self.update() self.rowconfigure(1, weight=1) self.minsize(10, 10) self.protocol("WM_DELETE_WINDOW", self.hide) # --- style self.style = Style(self) self.style.configure(self.id + ".TCheckbutton", selectbackground="red") self.style.map('TEntry', selectbackground=[('!focus', '#c3c3c3')]) selectbg = self.style.lookup('TEntry', 'selectbackground', ('focus', )) self.style.configure("sel.TCheckbutton", background=selectbg) self.style.map("sel.TCheckbutton", background=[("active", selectbg)]) # --- note elements # title font_title = "%s %s" % (CONFIG.get("Font", "title_family").replace( " ", "\ "), CONFIG.get("Font", "title_size")) style = CONFIG.get("Font", "title_style").split(",") if style: font_title += " " font_title += " ".join(style) self.title_var = StringVar(master=self, value=kwargs.get("title", _("Title"))) self.title_label = Label(self, textvariable=self.title_var, anchor="center", style=self.id + ".TLabel", font=font_title) self.title_entry = Entry(self, textvariable=self.title_var, exportselection=False, justify="center", font=font_title) # buttons/icons self.roll = Label(self, image="img_roll", style=self.id + ".TLabel") self.close = Label(self, image="img_close", style=self.id + ".TLabel") self.im_lock = PhotoImage(master=self, file=IM_LOCK) self.cadenas = Label(self, style=self.id + ".TLabel") # corner grip self.corner = Sizegrip(self, style=self.id + ".TSizegrip") # texte font_text = "%s %s" % (CONFIG.get("Font", "text_family").replace( " ", "\ "), CONFIG.get("Font", "text_size")) self.txt = Text(self, wrap='word', undo=True, selectforeground='white', inactiveselectbackground=selectbg, selectbackground=selectbg, tabs=(10, 'right', 21, 'left'), relief="flat", borderwidth=0, highlightthickness=0, font=font_text) # tags self.txt.tag_configure("bold", font="%s bold" % font_text) self.txt.tag_configure("italic", font="%s italic" % font_text) self.txt.tag_configure("bold-italic", font="%s bold italic" % font_text) self.txt.tag_configure("underline", underline=True, selectforeground="white") self.txt.tag_configure("overstrike", overstrike=True, selectforeground="white") self.txt.tag_configure("center", justify="center") self.txt.tag_configure("left", justify="left") self.txt.tag_configure("right", justify="right") self.txt.tag_configure("link", foreground="blue", underline=True, selectforeground="white") self.txt.tag_configure("list", lmargin1=0, lmargin2=21, tabs=(10, 'right', 21, 'left')) self.txt.tag_configure("todolist", lmargin1=0, lmargin2=21, tabs=(10, 'right', 21, 'left')) margin = 2 * Font(self, font=font_text).measure("m") self.txt.tag_configure("enum", lmargin1=0, lmargin2=margin + 5, tabs=(margin, 'right', margin + 5, 'left')) for coul in TEXT_COLORS.values(): self.txt.tag_configure(coul, foreground=coul, selectforeground="white") self.txt.tag_configure(coul + "-underline", foreground=coul, selectforeground="white", underline=True) self.txt.tag_configure(coul + "-overstrike", foreground=coul, overstrike=True, selectforeground="white") # --- menus # --- * menu on title self.menu = Menu(self, tearoff=False) # note color menu_note_color = Menu(self.menu, tearoff=False) colors = list(COLORS.keys()) colors.sort() for coul in colors: menu_note_color.add_command( label=coul, command=lambda key=coul: self.change_color(key)) # category self.category = StringVar( self, kwargs.get("category", CONFIG.get("General", "default_category"))) self.menu_categories = Menu(self.menu, tearoff=False) categories = CONFIG.options("Categories") categories.sort() for cat in categories: self.menu_categories.add_radiobutton(label=cat.capitalize(), value=cat, variable=self.category, command=self.change_category) # position: normal, always above, always below self.position = StringVar( self, kwargs.get("position", CONFIG.get("General", "position"))) menu_position = Menu(self.menu, tearoff=False) menu_position.add_radiobutton(label=_("Always above"), value="above", variable=self.position, command=self.set_position_above) menu_position.add_radiobutton(label=_("Always below"), value="below", variable=self.position, command=self.set_position_below) menu_position.add_radiobutton(label=_("Normal"), value="normal", variable=self.position, command=self.set_position_normal) # mode: note, list, todo list menu_mode = Menu(self.menu, tearoff=False) self.mode = StringVar(self, kwargs.get("mode", "note")) menu_mode.add_radiobutton(label=_("Note"), value="note", variable=self.mode, command=self.set_mode_note) menu_mode.add_radiobutton(label=_("List"), value="list", variable=self.mode, command=self.set_mode_list) menu_mode.add_radiobutton(label=_("ToDo List"), value="todolist", variable=self.mode, command=self.set_mode_todolist) menu_mode.add_radiobutton(label=_("Enumeration"), value="enum", variable=self.mode, command=self.set_mode_enum) self.menu.add_command(label=_("Delete"), command=self.delete) self.menu.add_cascade(label=_("Category"), menu=self.menu_categories) self.menu.add_cascade(label=_("Color"), menu=menu_note_color) self.menu.add_command(label=_("Lock"), command=self.lock) self.menu.add_cascade(label=_("Position"), menu=menu_position) self.menu.add_cascade(label=_("Mode"), menu=menu_mode) # --- * menu on main text self.menu_txt = Menu(self.txt, tearoff=False) # style menu_style = Menu(self.menu_txt, tearoff=False) menu_style.add_command(label=_("Bold"), command=lambda: self.toggle_text_style("bold")) menu_style.add_command( label=_("Italic"), command=lambda: self.toggle_text_style("italic")) menu_style.add_command(label=_("Underline"), command=self.toggle_underline) menu_style.add_command(label=_("Overstrike"), command=self.toggle_overstrike) # text alignment menu_align = Menu(self.menu_txt, tearoff=False) menu_align.add_command(label=_("Left"), command=lambda: self.set_align("left")) menu_align.add_command(label=_("Right"), command=lambda: self.set_align("right")) menu_align.add_command(label=_("Center"), command=lambda: self.set_align("center")) # text color menu_colors = Menu(self.menu_txt, tearoff=False) colors = list(TEXT_COLORS.keys()) colors.sort() for coul in colors: menu_colors.add_command(label=coul, command=lambda key=coul: self. change_sel_color(TEXT_COLORS[key])) # insert menu_insert = Menu(self.menu_txt, tearoff=False) menu_insert.add_command(label=_("Symbols"), command=self.add_symbols) menu_insert.add_command(label=_("Checkbox"), command=self.add_checkbox) menu_insert.add_command(label=_("Image"), command=self.add_image) menu_insert.add_command(label=_("Date"), command=self.add_date) menu_insert.add_command(label=_("Link"), command=self.add_link) if LATEX: menu_insert.add_command(label="LaTex", command=self.add_latex) self.menu_txt.add_cascade(label=_("Style"), menu=menu_style) self.menu_txt.add_cascade(label=_("Alignment"), menu=menu_align) self.menu_txt.add_cascade(label=_("Color"), menu=menu_colors) self.menu_txt.add_cascade(label=_("Insert"), menu=menu_insert) # --- restore note content/appearence self.color = kwargs.get("color", CONFIG.get("Categories", self.category.get())) self.txt.insert('1.0', kwargs.get("txt", "")) self.txt.edit_reset() # clear undo stack # restore inserted objects (images and checkboxes) # we need to restore objects with increasing index to avoid placment errors indexes = list(kwargs.get("inserted_objects", {}).keys()) indexes.sort(key=sorting) for index in indexes: kind, val = kwargs["inserted_objects"][index] if kind == "checkbox": ch = Checkbutton(self.txt, takefocus=False, style=self.id + ".TCheckbutton") if val: ch.state(("selected", )) self.txt.window_create(index, window=ch) elif kind == "image": if os.path.exists(val): self.images.append(PhotoImage(master=self.txt, file=val)) self.txt.image_create(index, image=self.images[-1], name=val) # restore tags for tag, indices in kwargs.get("tags", {}).items(): if indices: self.txt.tag_add(tag, *indices) for link in kwargs.get("links", {}).values(): self.nb_links += 1 self.links[self.nb_links] = link self.txt.tag_bind("link#%i" % self.nb_links, "<Button-1>", lambda e, l=link: open_url(l)) for img, latex in kwargs.get("latex", {}).items(): self.latex[img] = latex if LATEX: self.txt.tag_bind(img, '<Double-Button-1>', lambda e, im=img: self.add_latex(im)) mode = self.mode.get() if mode != "note": self.txt.tag_add(mode, "1.0", "end") self.txt.focus_set() self.lock() if kwargs.get("rolled", False): self.rollnote() if self.position.get() == "above": self.set_position_above() elif self.position.get() == "below": self.set_position_below() # --- placement # titlebar if CONFIG.get("General", "buttons_position") == "right": # right = lock icon - title - roll - close self.columnconfigure(1, weight=1) self.roll.grid(row=0, column=2, sticky="e") self.close.grid(row=0, column=3, sticky="e", padx=(0, 2)) self.cadenas.grid(row=0, column=0, sticky="w") self.title_label.grid(row=0, column=1, sticky="ew", pady=(1, 0)) else: # left = close - roll - title - lock icon self.columnconfigure(2, weight=1) self.roll.grid(row=0, column=1, sticky="w") self.close.grid(row=0, column=0, sticky="w", padx=(2, 0)) self.cadenas.grid(row=0, column=3, sticky="e") self.title_label.grid(row=0, column=2, sticky="ew", pady=(1, 0)) # body self.txt.grid(row=1, columnspan=4, column=0, sticky="ewsn", pady=(1, 4), padx=4) self.corner.lift(self.txt) self.corner.place(relx=1.0, rely=1.0, anchor="se") # --- bindings self.bind("<FocusOut>", self.save_note) self.bind('<Configure>', self.bouge) self.bind('<Button-1>', self.change_focus, True) self.close.bind("<Button-1>", self.hide) self.close.bind("<Enter>", self.enter_close) self.close.bind("<Leave>", self.leave_close) self.roll.bind("<Button-1>", self.rollnote) self.roll.bind("<Enter>", self.enter_roll) self.roll.bind("<Leave >", self.leave_roll) self.title_label.bind("<Double-Button-1>", self.edit_title) self.title_label.bind("<ButtonPress-1>", self.start_move) self.title_label.bind("<ButtonRelease-1>", self.stop_move) self.title_label.bind("<B1-Motion>", self.move) self.title_label.bind('<Button-3>', self.show_menu) self.title_entry.bind("<Return>", lambda e: self.title_entry.place_forget()) self.title_entry.bind("<FocusOut>", lambda e: self.title_entry.place_forget()) self.title_entry.bind("<Escape>", lambda e: self.title_entry.place_forget()) self.txt.tag_bind("link", "<Enter>", lambda event: self.txt.configure(cursor="hand1")) self.txt.tag_bind("link", "<Leave>", lambda event: self.txt.configure(cursor="")) self.txt.bind("<FocusOut>", self.save_note) self.txt.bind('<Button-3>', self.show_menu_txt) # add binding to the existing class binding so that the selected text # is erased on pasting self.txt.bind("<Control-v>", self.paste) self.corner.bind('<ButtonRelease-1>', self.resize) # --- keyboard shortcuts self.txt.bind('<Control-b>', lambda e: self.toggle_text_style('bold')) self.txt.bind('<Control-i>', lambda e: self.toggle_text_style('italic')) self.txt.bind('<Control-u>', lambda e: self.toggle_underline()) self.txt.bind('<Control-r>', lambda e: self.set_align('right')) self.txt.bind('<Control-l>', lambda e: self.set_align('left')) def __setattr__(self, name, value): object.__setattr__(self, name, value) if name == "color": self.style.configure(self.id + ".TSizegrip", background=self.color) self.style.configure(self.id + ".TLabel", background=self.color) self.style.configure("close" + self.id + ".TLabel", background=self.color) self.style.configure("roll" + self.id + ".TLabel", background=self.color) self.style.map(self.id + ".TLabel", background=[("active", self.color)]) self.style.configure(self.id + ".TCheckbutton", background=self.color) self.style.map(self.id + ".TCheckbutton", background=[("active", self.color), ("disabled", self.color)]) self.style.map("close" + self.id + ".TLabel", background=[("active", self.color)]) self.style.map("roll" + self.id + ".TLabel", background=[("active", self.color)]) self.configure(bg=self.color) self.txt.configure(bg=self.color) def paste(self, event): """ delete selected text before pasting """ if self.txt.tag_ranges("sel"): self.txt.delete("sel.first", "sel.last") def delete(self, confirmation=True): """ Delete this note """ if confirmation: rep = askokcancel(_("Confirmation"), _("Delete the note?")) else: rep = True if rep: del (self.master.note_data[self.id]) del (self.master.notes[self.id]) self.master.save() self.destroy() def lock(self): """ Put note in read-only mode to avoid unwanted text insertion """ if self.is_locked: selectbg = self.style.lookup('TEntry', 'selectbackground', ('focus', )) self.txt.configure(state="normal", selectforeground='white', selectbackground=selectbg, inactiveselectbackground=selectbg) self.style.configure("sel.TCheckbutton", background=selectbg) self.style.map("sel.TCheckbutton", background=[("active", selectbg)]) self.is_locked = False for checkbox in self.txt.window_names(): ch = self.txt.children[checkbox.split(".")[-1]] ch.configure(state="normal") self.cadenas.configure(image="") self.menu.entryconfigure(3, label=_("Lock")) self.title_label.bind("<Double-Button-1>", self.edit_title) self.txt.bind('<Button-3>', self.show_menu_txt) else: self.txt.configure(state="disabled", selectforeground='black', inactiveselectbackground='#c3c3c3', selectbackground='#c3c3c3') self.style.configure("sel.TCheckbutton", background='#c3c3c3') self.style.map("sel.TCheckbutton", background=[("active", '#c3c3c3')]) self.cadenas.configure(image=self.im_lock) for checkbox in self.txt.window_names(): ch = self.txt.children[checkbox.split(".")[-1]] ch.configure(state="disabled") self.is_locked = True self.menu.entryconfigure(3, label=_("Unlock")) self.title_label.unbind("<Double-Button-1>") self.txt.unbind('<Button-3>') self.save_note() def save_info(self): """ Return the dictionnary containing all the note data """ data = {} data["txt"] = self.txt.get("1.0", "end")[:-1] data["tags"] = {} for tag in self.txt.tag_names(): if tag not in ["sel", "todolist", "list", "enum"]: data["tags"][tag] = [ index.string for index in self.txt.tag_ranges(tag) ] data["title"] = self.title_var.get() data["geometry"] = self.save_geometry data["category"] = self.category.get() data["color"] = self.color data["locked"] = self.is_locked data["mode"] = self.mode.get() data["inserted_objects"] = {} data["rolled"] = not self.txt.winfo_ismapped() data["position"] = self.position.get() data["links"] = {} for i, link in self.links.items(): if self.txt.tag_ranges("link#%i" % i): data["links"][i] = link data["latex"] = {} for img, latex in self.latex.items(): if self.txt.tag_ranges(img): data["latex"][img] = latex for image in self.txt.image_names(): data["inserted_objects"][self.txt.index(image)] = ( "image", image.split('#')[0]) for checkbox in self.txt.window_names(): ch = self.txt.children[checkbox.split(".")[-1]] data["inserted_objects"][self.txt.index(checkbox)] = ( "checkbox", "selected" in ch.state()) return data def change_color(self, key): self.color = COLORS[key] self.save_note() def change_category(self, category=None): if category: self.category.set(category) self.color = CONFIG.get("Categories", self.category.get()) self.save_note() def set_position_above(self): e = ewmh.EWMH() for w in e.getClientList(): if w.get_wm_name() == 'mynotes%s' % self.id: e.setWmState(w, 1, '_NET_WM_STATE_ABOVE') e.setWmState(w, 0, '_NET_WM_STATE_BELOW') e.display.flush() self.save_note() def set_position_below(self): e = ewmh.EWMH() for w in e.getClientList(): if w.get_wm_name() == 'mynotes%s' % self.id: e.setWmState(w, 0, '_NET_WM_STATE_ABOVE') e.setWmState(w, 1, '_NET_WM_STATE_BELOW') e.display.flush() self.save_note() def set_position_normal(self): e = ewmh.EWMH() for w in e.getClientList(): if w.get_wm_name() == 'mynotes%s' % self.id: e.setWmState(w, 0, '_NET_WM_STATE_BELOW') e.setWmState(w, 0, '_NET_WM_STATE_ABOVE') e.display.flush() self.save_note() def set_mode_note(self): self.txt.tag_remove("list", "1.0", "end") self.txt.tag_remove("todolist", "1.0", "end") self.txt.tag_remove("enum", "1.0", "end") self.save_note() def set_mode_list(self): end = int(self.txt.index("end").split(".")[0]) lines = self.txt.get("1.0", "end").splitlines() for i, l in zip(range(1, end), lines): # remove checkboxes try: ch = self.txt.window_cget("%i.0" % i, "window") self.txt.children[ch.split('.')[-1]].destroy() self.txt.delete("%i.0" % i) except TclError: # there is no checkbox # remove enumeration res = re.match('^\t[0-9]+\.\t', l) if res: self.txt.delete("%i.0" % i, "%i.%i" % (i, res.end())) if self.txt.get("%i.0" % i, "%i.3" % i) != "\t•\t": self.txt.insert("%i.0" % i, "\t•\t") self.txt.tag_add("list", "1.0", "end") self.txt.tag_remove("todolist", "1.0", "end") self.txt.tag_remove("enum", "1.0", "end") self.save_note() def set_mode_enum(self): self.txt.configure(autoseparators=False) self.txt.edit_separator() end = int(self.txt.index("end").split(".")[0]) lines = self.txt.get("1.0", "end").splitlines() for i, l in zip(range(1, end), lines): # remove checkboxes try: ch = self.txt.window_cget("%i.0" % i, "window") self.txt.children[ch.split('.')[-1]].destroy() self.txt.delete("%i.0" % i) except TclError: # there is no checkbox # remove bullets if self.txt.get("%i.0" % i, "%i.3" % i) == "\t•\t": self.txt.delete("%i.0" % i, "%i.3" % i) if not re.match('^\t[0-9]+\.', l): self.txt.insert("%i.0" % i, "\t0.\t") self.txt.tag_add("enum", "1.0", "end") self.txt.tag_remove("todolist", "1.0", "end") self.txt.tag_remove("list", "1.0", "end") self.update_enum() self.txt.configure(autoseparators=True) self.txt.edit_separator() self.save_note() def set_mode_todolist(self): end = int(self.txt.index("end").split(".")[0]) lines = self.txt.get("1.0", "end").splitlines() for i, l in zip(range(1, end), lines): res = re.match('^\t[0-9]+\.\t', l) if res: self.txt.delete("%i.0" % i, "%i.%i" % (i, res.end())) elif self.txt.get("%i.0" % i, "%i.3" % i) == "\t•\t": self.txt.delete("%i.0" % i, "%i.3" % i) try: ch = self.txt.window_cget("%i.0" % i, "window") except TclError: ch = Checkbutton(self.txt, takefocus=False, style=self.id + ".TCheckbutton") self.txt.window_create("%i.0" % i, window=ch) self.txt.tag_remove("enum", "1.0", "end") self.txt.tag_remove("list", "1.0", "end") self.txt.tag_add("todolist", "1.0", "end") self.save_note() # --- bindings def enter_roll(self, event): """ mouse is over the roll icon """ self.roll.configure(image="img_rollactive") def leave_roll(self, event): """ mouse leaves the roll icon """ self.roll.configure(image="img_roll") def enter_close(self, event): """ mouse is over the close icon """ self.close.configure(image="img_closeactive") def leave_close(self, event): """ mouse leaves the close icon """ self.close.configure(image="img_close") def change_focus(self, event): if not self.is_locked: event.widget.focus_force() def show_menu(self, event): self.menu.tk_popup(event.x_root, event.y_root) def show_menu_txt(self, event): self.menu_txt.tk_popup(event.x_root, event.y_root) def resize(self, event): self.save_geometry = self.geometry() def bouge(self, event): geo = self.geometry().split("+")[1:] self.save_geometry = self.save_geometry.split("+")[0] \ + "+%s+%s" % tuple(geo) def edit_title(self, event): self.title_entry.place(x=self.title_label.winfo_x() + 5, y=self.title_label.winfo_y(), anchor="nw", width=self.title_label.winfo_width() - 10) def start_move(self, event): self.x = event.x self.y = event.y self.configure(cursor='fleur') def stop_move(self, event): self.x = None self.y = None self.configure(cursor='') def move(self, event): if self.x is not None and self.y is not None: deltax = event.x - self.x deltay = event.y - self.y x = self.winfo_x() + deltax y = self.winfo_y() + deltay self.geometry("+%s+%s" % (x, y)) def save_note(self, event=None): data = self.save_info() data["visible"] = True self.master.note_data[self.id] = data self.master.save() def rollnote(self, event=None): if self.txt.winfo_ismapped(): self.txt.grid_forget() self.corner.place_forget() self.geometry("%sx22" % self.winfo_width()) else: self.txt.grid(row=1, columnspan=4, column=0, sticky="ewsn", pady=(1, 4), padx=4) self.corner.place(relx=1.0, rely=1.0, anchor="se") self.geometry(self.save_geometry) self.save_note() def hide(self, event=None): """ Hide note (can be displayed again via app menu) """ cat = self.category.get() self.master.add_note_to_menu(self.id, self.title_var.get().strip(), cat) data = self.save_info() data["visible"] = False self.master.note_data[self.id] = data del (self.master.notes[self.id]) self.master.save() self.destroy() # --- Settings update def update_title_font(self): font = "%s %s" % (CONFIG.get("Font", "title_family").replace( " ", "\ "), CONFIG.get("Font", "title_size")) style = CONFIG.get("Font", "title_style").split(",") if style: font += " " font += " ".join(style) self.title_label.configure(font=font) def update_text_font(self): font = "%s %s" % (CONFIG.get("Font", "text_family").replace( " ", "\ "), CONFIG.get("Font", "text_size")) self.txt.configure(font=font) self.txt.tag_configure("bold", font="%s bold" % font) self.txt.tag_configure("italic", font="%s italic" % font) self.txt.tag_configure("bold-italic", font="%s bold italic" % font) margin = 2 * Font(self, font=font).measure("m") self.txt.tag_configure("enum", lmargin1=0, lmargin2=margin + 5, tabs=(margin, 'right', margin + 5, 'left')) def update_menu_cat(self, categories): """ Update the category submenu """ self.menu_categories.delete(0, "end") for cat in categories: self.menu_categories.add_radiobutton(label=cat.capitalize(), value=cat, variable=self.category, command=self.change_category) def update_titlebar(self): if CONFIG.get("General", "buttons_position") == "right": # right = lock icon - title - roll - close self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=0) self.roll.grid_configure(row=0, column=2, sticky="e") self.close.grid_configure(row=0, column=3, sticky="e", padx=(0, 2)) self.cadenas.grid_configure(row=0, column=0, sticky="w") self.title_label.grid_configure(row=0, column=1, sticky="ew", pady=(1, 0)) else: # left = close - roll - title - lock icon self.columnconfigure(2, weight=1) self.columnconfigure(1, weight=0) self.roll.grid_configure(row=0, column=1, sticky="w") self.close.grid_configure(row=0, column=0, sticky="w", padx=(2, 0)) self.cadenas.grid_configure(row=0, column=3, sticky="e") self.title_label.grid_configure(row=0, column=2, sticky="ew", pady=(1, 0)) # --- Text edition def add_link(self): def ok(eveny=None): lien = link.get() txt = text.get() if lien: if not txt: txt = lien self.nb_links += 1 if self.txt.tag_ranges("sel"): index = self.txt.index("sel.first") self.txt.delete('sel.first', 'sel.last') else: index = "current" tags = self.txt.tag_names(index) + ("link", "link#%i" % self.nb_links) self.txt.insert("current", txt, tags) if not lien[:4] == "http": lien = "http://" + lien self.links[self.nb_links] = lien self.txt.tag_bind("link#%i" % self.nb_links, "<Button-1>", lambda e: open_url(lien)) top.destroy() top = Toplevel(self) top.transient(self) top.update_idletasks() top.geometry("+%i+%i" % top.winfo_pointerxy()) top.grab_set() top.resizable(True, False) top.title(_("Link")) top.columnconfigure(1, weight=1) text = Entry(top) link = Entry(top) if self.txt.tag_ranges('sel'): txt = self.txt.get('sel.first', 'sel.last') else: txt = '' text.insert(0, txt) text.icursor("end") Label(top, text=_("Text")).grid(row=0, column=0, sticky="e", padx=4, pady=4) Label(top, text=_("Link")).grid(row=1, column=0, sticky="e", padx=4, pady=4) text.grid(row=0, column=1, sticky="ew", padx=4, pady=4) link.grid(row=1, column=1, sticky="ew", padx=4, pady=4) Button(top, text="Ok", command=ok).grid(row=2, columnspan=2, padx=4, pady=4) text.focus_set() text.bind("<Return>", ok) link.bind("<Return>", ok) def add_checkbox(self): ch = Checkbutton(self.txt, takefocus=False, style=self.id + ".TCheckbutton") self.txt.window_create("current", window=ch) def add_date(self): self.txt.insert("current", strftime("%x")) def add_latex(self, img_name=None): def ok(event): latex = r'%s' % text.get() if latex: if img_name is None: l = [ int(os.path.splitext(f)[0]) for f in os.listdir(PATH_LATEX) ] l.sort() if l: i = l[-1] + 1 else: i = 0 img = "%i.png" % i self.txt.tag_bind(img, '<Double-Button-1>', lambda e: self.add_latex(img)) self.latex[img] = latex else: img = img_name im = os.path.join(PATH_LATEX, img) try: math_to_image(latex, im, fontsize=CONFIG.getint("Font", "text_size") - 2) self.images.append(PhotoImage(file=im, master=self)) if self.txt.tag_ranges("sel"): index = self.txt.index("sel.first") self.txt.delete('sel.first', 'sel.last') else: index = self.txt.index("current") self.txt.image_create(index, image=self.images[-1], name=im) self.txt.tag_add(img, index) top.destroy() except Exception as e: showerror(_("Error"), str(e)) top = Toplevel(self) top.transient(self) top.update_idletasks() top.geometry("+%i+%i" % top.winfo_pointerxy()) top.grab_set() top.resizable(True, False) top.title("LaTex") text = Entry(top, justify='center') if img_name is not None: text.insert(0, self.latex[img_name]) else: if self.txt.tag_ranges('sel'): text.insert(0, self.txt.get('sel.first', 'sel.last')) else: text.insert(0, '$$') text.icursor(1) text.pack(fill='x', expand=True) text.bind('<Return>', ok) text.focus_set() def add_image(self): fichier = askopenfilename(defaultextension=".png", filetypes=[("PNG", "*.png")], initialdir="", initialfile="", title=_('Select PNG image')) if os.path.exists(fichier): self.images.append(PhotoImage(master=self.txt, file=fichier)) self.txt.image_create("current", image=self.images[-1], name=fichier) elif fichier: showerror("Erreur", "L'image %s n'existe pas" % fichier) def add_symbols(self): symbols = pick_symbol( self, CONFIG.get("Font", "text_family").replace(" ", "\ "), CONFIG.get("General", "symbols")) self.txt.insert("current", symbols) def toggle_text_style(self, style): '''Toggle the style of the selected text''' if self.txt.tag_ranges("sel"): current_tags = self.txt.tag_names("sel.first") if style in current_tags: # first char is in style so 'unstyle' the range self.txt.tag_remove(style, "sel.first", "sel.last") elif style == "bold" and "bold-italic" in current_tags: self.txt.tag_remove("bold-italic", "sel.first", "sel.last") self.txt.tag_add("italic", "sel.first", "sel.last") elif style == "italic" and "bold-italic" in current_tags: self.txt.tag_remove("bold-italic", "sel.first", "sel.last") self.txt.tag_add("bold", "sel.first", "sel.last") elif style == "bold" and "italic" in current_tags: self.txt.tag_remove("italic", "sel.first", "sel.last") self.txt.tag_add("bold-italic", "sel.first", "sel.last") elif style == "italic" and "bold" in current_tags: self.txt.tag_remove("bold", "sel.first", "sel.last") self.txt.tag_add("bold-italic", "sel.first", "sel.last") else: # first char is normal, so apply style to the whole selection self.txt.tag_add(style, "sel.first", "sel.last") def toggle_underline(self): if self.txt.tag_ranges("sel"): current_tags = self.txt.tag_names("sel.first") if "underline" in current_tags: # first char is in style so 'unstyle' the range self.txt.tag_remove("underline", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): self.txt.tag_remove(coul + "-underline", "sel.first", "sel.last") else: self.txt.tag_add("underline", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): r = text_ranges(self.txt, coul, "sel.first", "sel.last") if r: for deb, fin in zip(r[::2], r[1::2]): self.txt.tag_add(coul + "-underline", "sel.first", "sel.last") def toggle_overstrike(self): if self.txt.tag_ranges("sel"): current_tags = self.txt.tag_names("sel.first") if "overstrike" in current_tags: # first char is in style so 'unstyle' the range self.txt.tag_remove("overstrike", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): self.txt.tag_remove(coul + "-overstrike", "sel.first", "sel.last") else: self.txt.tag_add("overstrike", "sel.first", "sel.last") for coul in TEXT_COLORS.values(): r = text_ranges(self.txt, coul, "sel.first", "sel.last") if r: for deb, fin in zip(r[::2], r[1::2]): self.txt.tag_add(coul + "-overstrike", "sel.first", "sel.last") def change_sel_color(self, color): """ change the color of the selection """ if self.txt.tag_ranges("sel"): for coul in TEXT_COLORS.values(): self.txt.tag_remove(coul, "sel.first", "sel.last") self.txt.tag_remove(coul + "-overstrike", "sel.first", "sel.last") self.txt.tag_remove(coul + "-underline", "sel.first", "sel.last") if not color == "black": self.txt.tag_add(color, "sel.first", "sel.last") underline = text_ranges(self.txt, "underline", "sel.first", "sel.last") overstrike = text_ranges(self.txt, "overstrike", "sel.first", "sel.last") for deb, fin in zip(underline[::2], underline[1::2]): self.txt.tag_add(color + "-underline", deb, fin) for deb, fin in zip(overstrike[::2], overstrike[1::2]): self.txt.tag_add(color + "-overstrike", deb, fin) def set_align(self, alignment): """ Align the text according to alignment (left, right, center) """ if self.txt.tag_ranges("sel"): line = self.txt.index("sel.first").split(".")[0] line2 = self.txt.index("sel.last").split(".")[0] deb, fin = line + ".0", line2 + ".end" if not "\t" in self.txt.get(deb, fin): # tabulations don't support right/center alignment # remove old alignment tag self.txt.tag_remove("left", deb, fin) self.txt.tag_remove("right", deb, fin) self.txt.tag_remove("center", deb, fin) # set new alignment tag self.txt.tag_add(alignment, deb, fin) def update_enum(self): """ update enumeration numbers """ lines = self.txt.get("1.0", "end").splitlines() indexes = [] for i, l in enumerate(lines): res = re.match('^\t[0-9]+\.\t', l) res2 = re.match('^\t[0-9]+\.', l) if res: indexes.append((i, res.end())) elif res2: indexes.append((i, res2.end())) for j, (i, end) in enumerate(indexes): self.txt.delete("%i.0" % (i + 1), "%i.%i" % (i + 1, end)) self.txt.insert("%i.0" % (i + 1), "\t%i.\t" % (j + 1)) self.txt.tag_add("enum", "1.0", "end")
class App(Tk): def __init__(self): Tk.__init__(self, className=cst.APP_NAME) self.protocol("WM_DELETE_WINDOW", self.quit) self.withdraw() logging.info('Starting %s', cst.APP_NAME) self.im_icon = PhotoImage(master=self, file=cst.IM_ICON_48) self.iconphoto(True, self.im_icon) # --- style self.style = Style(self) self.style.theme_use("clam") self.style.configure("TScale", sliderlength=20) self.style.map("TCombobox", fieldbackground=[('readonly', 'white')], selectbackground=[('readonly', 'white')], selectforeground=[('readonly', 'black')]) self.style.configure("title.TLabel", font="TkDefaultFont 9 bold") self.style.configure("white.TLabel", background="white") self.style.map("white.TLabel", background=[("active", "white")]) self.style.configure('heading.TLabel', relief='ridge', borderwidth=1, padding=(10, 4)) self.style.configure('manager.TButton', padding=0) self.style.map('manager.Treeview', background=[], foreground=[]) self.style.layout( 'no_edit.TEntry', [('Entry.padding', { 'children': [('Entry.textarea', { 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('no_edit.TEntry', background='white', padding=[4, 0]) self.style.configure('manager.TEntry', padding=[2, 1]) self.style.layout('manager.Treeview.Row', [('Treeitem.row', { 'sticky': 'nswe' }), ('Treeitem.image', { 'side': 'right', 'sticky': 'e' })]) self.style.layout('manager.Treeview.Item', [('Treeitem.padding', { 'children': [('Checkbutton.indicator', { 'side': 'left', 'sticky': '' }), ('Treeitem.text', { 'side': 'left', 'sticky': '' })], 'sticky': 'nswe' })]) self._im_trough = tkPhotoImage(name='trough-scrollbar-vert', width=15, height=15, master=self) bg = CONFIG.get("Widget", 'background', fallback='gray10') widget_bg = (0, 0, 0) widget_fg = (255, 255, 255) vmax = self.winfo_rgb('white')[0] color = tuple(int(val / vmax * 255) for val in widget_bg) active_bg = cst.active_color(color) active_bg2 = cst.active_color(cst.active_color(color, 'RGB')) slider_vert_insens = Image.new('RGBA', (13, 28), widget_bg) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) self._im_trough.put(" ".join(["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active = PhotoImage(slider_vert_active, name='slider-vert-active', master=self) self._im_slider_vert = PhotoImage(slider_vert, name='slider-vert', master=self) self._im_slider_vert_prelight = PhotoImage(slider_vert_prelight, name='slider-vert-prelight', master=self) self._im_slider_vert_insens = PhotoImage(slider_vert_insens, name='slider-vert-insens', master=self) self.style.element_create('widget.Vertical.Scrollbar.trough', 'image', 'trough-scrollbar-vert') self.style.element_create( 'widget.Vertical.Scrollbar.thumb', 'image', 'slider-vert', ('pressed', '!disabled', 'slider-vert-active'), ('active', '!disabled', 'slider-vert-prelight'), ('disabled', 'slider-vert-insens'), border=6, sticky='ns') self.style.layout('widget.Vertical.TScrollbar', [ ('widget.Vertical.Scrollbar.trough', { 'children': [('widget.Vertical.Scrollbar.thumb', { 'expand': '1' })], 'sticky': 'ns' }) ]) hide = Image.new('RGBA', (12, 12), active_bg2) hide_active = Image.new('RGBA', (12, 12), widget_fg) hide_pressed = Image.new('RGBA', (12, 12), (150, 0, 0)) toggle_open = Image.new('RGBA', (9, 9), widget_fg) toggle_open_active = Image.new('RGBA', (9, 9), active_bg2) toggle_close = Image.new('RGBA', (9, 9), widget_fg) toggle_close_active = Image.new('RGBA', (9, 9), active_bg2) self._im_hide = PhotoImage(hide, master=self) self._im_hide_active = PhotoImage(hide_active, master=self) self._im_hide_pressed = PhotoImage(hide_pressed, master=self) self._im_open = PhotoImage(toggle_open, master=self) self._im_open_active = PhotoImage(toggle_open_active, master=self) self._im_close = PhotoImage(toggle_close, master=self) self._im_close_active = PhotoImage(toggle_close_active, master=self) self.style.element_create( "toggle", "image", self._im_close, ("!hover", "selected", "!disabled", self._im_open), ("hover", "!selected", "!disabled", self._im_close_active), ("hover", "selected", "!disabled", self._im_open_active), border=2, sticky='') self.style.layout('Toggle', [('Toggle.border', { 'children': [('Toggle.padding', { 'children': [('Toggle.toggle', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('widget.close.TButton', background=bg, relief='flat', image=self._im_hide, padding=0) self.style.map('widget.close.TButton', background=[], relief=[], image=[('active', '!pressed', self._im_hide_active), ('active', 'pressed', self._im_hide_pressed)]) self.option_add('*Toplevel.background', self.style.lookup('TFrame', 'background')) self.option_add('*{app_name}.background'.format(app_name=cst.APP_NAME), self.style.lookup('TFrame', 'background')) self.widget_style_init() # --- tray icon menu self.icon = TrayIcon(cst.ICON) self.menu_widgets = SubMenu(parent=self.icon.menu) self.menu_categories = SubMenu(parent=self.menu_widgets) self.menu_categories.add_command(label=_('Hide all'), command=self.hide_all_cats) self.menu_categories.add_command(label=_('Show all'), command=self.hide_all_cats) self.menu_categories.add_separator() self.menu_feeds = SubMenu(parent=self.menu_widgets) self.menu_feeds.add_command(label=_('Hide all'), command=self.hide_all_feeds) self.menu_feeds.add_command(label=_('Show all'), command=self.show_all_feeds) self.menu_feeds.add_separator() self.menu_widgets.add_command(label=_('Hide all'), command=self.hide_all) self.menu_widgets.add_command(label=_('Show all'), command=self.show_all) self.menu_widgets.add_separator() self.menu_widgets.add_cascade(label=_('Categories'), menu=self.menu_categories) self.menu_widgets.add_cascade(label=_('Feeds'), menu=self.menu_feeds) self.icon.menu.add_cascade(label=_('Widgets'), menu=self.menu_widgets) self.icon.menu.add_command(label=_('Add feed'), command=self.add) self.icon.menu.add_command(label=_('Update feeds'), command=self.feed_update) self.icon.menu.add_command(label=_('Manage feeds'), command=self.feed_manage) self.icon.menu.add_command(label=_("Suspend"), command=self.start_stop) self.icon.menu.add_separator() self.icon.menu.add_command(label=_('Settings'), command=self.settings) self.icon.menu.add_command(label=_("Check for updates"), command=lambda: UpdateChecker(self, True)) self.icon.menu.add_command(label=_("Help"), command=lambda: Help(self)) self.icon.menu.add_command(label=_("About"), command=lambda: About(self)) self.icon.menu.add_command(label=_('Quit'), command=self.quit) self.icon.loop(self) self._notify_no_internet = True self._internet_id = "" self._update_id = "" self._check_add_id = "" self._check_end_update_id = "" self._check_result_update_id = {} self._check_result_init_id = {} self.queues = {} self.threads = {} # --- category widgets self.cat_widgets = {} self.cat_widgets['All'] = CatWidget(self, 'All') self.cat_widgets['All'].event_generate('<Configure>') self.menu_widgets.add_checkbutton(label=_('Latests'), command=self.toggle_latests_widget) cst.add_trace(self.cat_widgets['All'].variable, 'write', self.latests_widget_trace) self.cat_widgets['All'].variable.set( LATESTS.getboolean('All', 'visible')) cats = LATESTS.sections() cats.remove('All') for category in cats: self.cat_widgets[category] = CatWidget(self, category) self.cat_widgets[category].event_generate('<Configure>') self.menu_categories.add_checkbutton( label=category, command=lambda c=category: self.toggle_category_widget(c)) cst.add_trace(self.cat_widgets[category].variable, 'write', lambda *args, c=category: self.cat_widget_trace(c)) self.cat_widgets[category].variable.set( LATESTS.getboolean(category, 'visible')) # --- feed widgets self.feed_widgets = {} for title in FEEDS.sections(): self._check_result_update_id[title] = '' self._check_result_init_id[title] = '' self.queues[title] = Queue(1) self.threads[title] = None self.menu_feeds.add_checkbutton( label=title, command=lambda t=title: self.toggle_feed_widget(t)) self.feed_widgets[title] = FeedWidget(self, title) cst.add_trace(self.feed_widgets[title].variable, 'write', lambda *args, t=title: self.feed_widget_trace(t)) self.feed_widgets[title].variable.set( FEEDS.getboolean(title, 'visible', fallback=True)) self.feed_init() # --- check for updates if CONFIG.getboolean("General", "check_update"): UpdateChecker(self) self.bind_class('TEntry', '<Control-a>', self.entry_select_all) def widget_style_init(self): """Init widgets style.""" bg = CONFIG.get('Widget', 'background', fallback='gray10') feed_bg = CONFIG.get('Widget', 'feed_background', fallback='gray20') fg = CONFIG.get('Widget', 'foreground') vmax = self.winfo_rgb('white')[0] widget_bg = tuple(int(val / vmax * 255) for val in self.winfo_rgb(bg)) widget_fg = tuple(int(val / vmax * 255) for val in self.winfo_rgb(fg)) active_bg = cst.active_color(widget_bg) active_bg2 = cst.active_color(cst.active_color(widget_bg, 'RGB')) slider_alpha = Image.open(cst.IM_SCROLL_ALPHA) slider_vert_insens = Image.new('RGBA', (13, 28), widget_bg) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert.putalpha(slider_alpha) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_active.putalpha(slider_alpha) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) slider_vert_prelight.putalpha(slider_alpha) self._im_slider_vert_active.paste(slider_vert_active) self._im_slider_vert.paste(slider_vert) self._im_slider_vert_prelight.paste(slider_vert_prelight) self._im_slider_vert_insens.paste(slider_vert_insens) self._im_trough.put(" ".join(["{" + " ".join([bg] * 15) + "}"] * 15)) hide_alpha = Image.open(cst.IM_HIDE_ALPHA) hide = Image.new('RGBA', (12, 12), active_bg) hide.putalpha(hide_alpha) hide_active = Image.new('RGBA', (12, 12), active_bg2) hide_active.putalpha(hide_alpha) hide_pressed = Image.new('RGBA', (12, 12), widget_fg) hide_pressed.putalpha(hide_alpha) toggle_open_alpha = Image.open(cst.IM_OPENED_ALPHA) toggle_open = Image.new('RGBA', (9, 9), widget_fg) toggle_open.putalpha(toggle_open_alpha) toggle_open_active = Image.new('RGBA', (9, 9), active_bg2) toggle_open_active.putalpha(toggle_open_alpha) toggle_close_alpha = Image.open(cst.IM_CLOSED_ALPHA) toggle_close = Image.new('RGBA', (9, 9), widget_fg) toggle_close.putalpha(toggle_close_alpha) toggle_close_active = Image.new('RGBA', (9, 9), active_bg2) toggle_close_active.putalpha(toggle_close_alpha) self._im_hide.paste(hide) self._im_hide_active.paste(hide_active) self._im_hide_pressed.paste(hide_pressed) self._im_open.paste(toggle_open) self._im_open_active.paste(toggle_open_active) self._im_close.paste(toggle_close) self._im_close_active.paste(toggle_close_active) self.style.configure('widget.TFrame', background=bg) self.style.configure('widget.close.TButton', background=bg) # relief='flat', image=self._im_hide, padding=0) # self.style.map('widget.close.TButton', background=[], relief=[], # image=[('active', '!pressed', self._im_hide_active), # ('active', 'pressed', self._im_hide_pressed)]) self.style.configure('widget.interior.TFrame', background=feed_bg) self.style.configure('widget.TSizegrip', background=bg) self.style.configure('widget.Horizontal.TSeparator', background=bg) self.style.configure('widget.TLabel', background=bg, foreground=fg, font=CONFIG.get('Widget', 'font')) self.style.configure('widget.title.TLabel', background=bg, foreground=fg, font=CONFIG.get('Widget', 'font_title')) self.style.configure('widget.TButton', background=bg, foreground=fg, padding=1, relief='flat') self.style.map('widget.TButton', background=[('disabled', active_bg), ('pressed', fg), ('active', active_bg)], foreground=[('pressed', bg)]) # relief=[('pressed', 'sunken')]) # bordercolor=[('pressed', active_bg)], # darkcolor=[('pressed', bg)], # lightcolor=[('pressed', fg)]) self.update_idletasks() def hide_all(self): """Withdraw all widgets.""" for widget in self.cat_widgets.values(): widget.withdraw() for widget in self.feed_widgets.values(): widget.withdraw() def show_all(self): """Deiconify all widgets.""" for widget in self.cat_widgets.values(): widget.deiconify() for widget in self.feed_widgets.values(): widget.deiconify() def hide_all_feeds(self): """Withdraw all feed widgets.""" for widget in self.feed_widgets.values(): widget.withdraw() def show_all_feeds(self): """Deiconify all feed widgets.""" for widget in self.feed_widgets.values(): widget.deiconify() def hide_all_cats(self): """Withdraw all category widgets.""" for cat, widget in self.cat_widgets.items(): if cat != 'All': widget.withdraw() def show_all_cats(self): """Deiconify all category widgets.""" for cat, widget in self.cat_widgets.items(): if cat != 'All': widget.deiconify() def start_stop(self): """Suspend / restart update checks.""" if self.icon.menu.get_item_label(4) == _("Suspend"): after_ids = [ self._update_id, self._check_add_id, self._internet_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass self.icon.menu.set_item_label(4, _("Restart")) self.icon.menu.disable_item(1) self.icon.menu.disable_item(2) self.icon.change_icon(cst.ICON_DISABLED, 'feedagregator suspended') else: self.icon.menu.set_item_label(4, _("Suspend")) self.icon.menu.enable_item(1) self.icon.menu.enable_item(2) self.icon.change_icon(cst.ICON, 'feedagregator') for widget in self.feed_widgets.values(): widget.clear() self.feed_init() @staticmethod def entry_select_all(event): event.widget.selection_clear() event.widget.selection_range(0, 'end') def test_connection(self): """ Launch update check if there is an internet connection otherwise check again for an internet connection after 30s. """ if cst.internet_on(): logging.info('Connected to Internet') self._notify_no_internet = True for widget in self.feed_widgets.values(): widget.clear() self.feed_init() else: self._internet_id = self.after(30000, self.test_connection) def quit(self): for after_id in self.tk.call('after', 'info'): try: self.after_cancel(after_id) except ValueError: pass for thread in self.threads.values(): try: thread.terminate() except AttributeError: pass for title, widget in self.feed_widgets.items(): FEEDS.set(title, 'visible', str(widget.variable.get())) for cat, widget in self.cat_widgets.items(): LATESTS.set(cat, 'visible', str(widget.variable.get())) try: self.destroy() except TclError: logging.error("Error on quit") self.after(500, self.quit) def feed_widget_trace(self, title): value = self.feed_widgets[title].variable.get() self.menu_feeds.set_item_value(title, value) FEEDS.set(title, 'visible', str(value)) cst.save_feeds() def cat_widget_trace(self, category): value = self.cat_widgets[category].variable.get() self.menu_categories.set_item_value(category, value) LATESTS.set(category, 'visible', str(value)) cst.save_latests() def latests_widget_trace(self, *args): value = self.cat_widgets['All'].variable.get() self.menu_widgets.set_item_value(_('Latests'), value) LATESTS.set('All', 'visible', str(value)) cst.save_latests() def toggle_category_widget(self, category): value = self.menu_categories.get_item_value(category) if value: self.cat_widgets[category].deiconify() else: self.cat_widgets[category].withdraw() self.update_idletasks() def toggle_latests_widget(self): value = self.menu_widgets.get_item_value(_('Latests')) if value: self.cat_widgets['All'].deiconify() else: self.cat_widgets['All'].withdraw() self.update_idletasks() def toggle_feed_widget(self, title): value = self.menu_feeds.get_item_value(title) if value: self.feed_widgets[title].deiconify() else: self.feed_widgets[title].withdraw() self.update_idletasks() def report_callback_exception(self, *args): """Log exceptions.""" err = "".join(traceback.format_exception(*args)) logging.error(err) showerror(_("Error"), str(args[1]), err, True) def settings(self): update_delay = CONFIG.get('General', 'update_delay') splash_supp = CONFIG.get('General', 'splash_supported', fallback=True) dialog = Config(self) self.wait_window(dialog) cst.save_config() self.widget_style_init() splash_change = splash_supp != CONFIG.get('General', 'splash_supported') for widget in self.cat_widgets.values(): widget.update_style() if splash_change: widget.update_position() for widget in self.feed_widgets.values(): widget.update_style() if splash_change: widget.update_position() if update_delay != CONFIG.get('General', 'update_delay'): self.feed_update() def add(self): dialog = Add(self) self.wait_window(dialog) url = dialog.url self.feed_add(url) def category_remove(self, category): self.cat_widgets[category].destroy() del self.cat_widgets[category] self.menu_categories.delete(category) LATESTS.remove_section(category) cst.save_feeds() cst.save_latests() @staticmethod def feed_get_info(url, queue, mode='latest'): feed = feedparser.parse(url) feed_title = feed['feed'].get('title', '') entries = feed['entries'] today = datetime.now().strftime('%Y-%m-%d %H:%M') if entries: entry_title = entries[0].get('title', '') summary = entries[0].get('summary', '') link = entries[0].get('link', '') latest = """<p id=title>{}</p>\n{}""".format(entry_title, summary) if 'updated' in entries[0]: updated = entries[0].get('updated') else: updated = entries[0].get('published', today) updated = dateutil.parser.parse( updated, tzinfos=cst.TZINFOS).strftime('%Y-%m-%d %H:%M') else: entry_title = "" summary = "" link = "" latest = "" updated = today if mode == 'all': data = [] for entry in entries: title = entry.get('title', '') summary = entry.get('summary', '') if 'updated' in entry: date = entry.get('updated') else: date = entry.get('published', today) date = dateutil.parser.parse( date, tzinfos=cst.TZINFOS).strftime('%Y-%m-%d %H:%M') link = entry.get('link', '') data.append((title, date, summary, link)) queue.put((feed_title, latest, updated, data)) else: queue.put( (feed_title, latest, updated, entry_title, summary, link)) def _check_result_add(self, thread, queue, url, manager_queue=None): if thread.is_alive(): self._check_add_id = self.after(1000, self._check_result_add, thread, queue, url, manager_queue) else: title, latest, date, data = queue.get(False) if title: try: # check if feed's title already exists FEEDS.add_section(title) except configparser.DuplicateSectionError: i = 2 duplicate = True while duplicate: # increment i until title~#i does not already exist try: FEEDS.add_section("{}~#{}".format(title, i)) except configparser.DuplicateSectionError: i += 1 else: duplicate = False name = "{}~#{}".format(title, i) else: name = title if manager_queue is not None: manager_queue.put(name) logging.info("Added feed '%s' %s", name, url) if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, name, cst.html2text(latest) ]) self.cat_widgets['All'].entry_add(name, date, latest, url) filename = cst.new_data_file() cst.save_data(filename, latest, data) FEEDS.set(name, 'url', url) FEEDS.set(name, 'updated', date) FEEDS.set(name, 'data', filename) FEEDS.set(name, 'visible', 'True') FEEDS.set(name, 'geometry', '') FEEDS.set(name, 'position', 'normal') FEEDS.set(name, 'category', '') FEEDS.set(name, 'sort_is_reversed', 'False') FEEDS.set(name, 'active', 'True') cst.save_feeds() self.queues[name] = queue self.feed_widgets[name] = FeedWidget(self, name) self.menu_feeds.add_checkbutton( label=name, command=lambda: self.toggle_feed_widget(name)) cst.add_trace(self.feed_widgets[name].variable, 'write', lambda *args: self.feed_widget_trace(name)) self.feed_widgets[name].variable.set(True) for entry_title, date, summary, link in data: self.feed_widgets[name].entry_add(entry_title, date, summary, link, -1) else: if manager_queue is not None: manager_queue.put('') if cst.internet_on(): logging.error('%s is not a valid feed.', url) showerror(_('Error'), _('{url} is not a valid feed.').format(url=url)) else: logging.warning('No Internet connection.') showerror(_('Error'), _('No Internet connection.')) def feed_add(self, url, manager=False): """ Add feed with given url. manager: whether this command is run from the feed manager. """ if url: queue = Queue(1) manager_queue = Queue(1) if manager else None thread = Process(target=self.feed_get_info, args=(url, queue, 'all'), daemon=True) thread.start() self._check_result_add(thread, queue, url, manager_queue) if manager: return manager_queue def feed_set_active(self, title, active): FEEDS.set(title, 'active', str(active)) cst.save_feeds() cat = FEEDS.get(title, 'category', fallback='') if active: self.menu_feeds.enable_item(title) if FEEDS.getboolean(title, 'visible'): self.feed_widgets[title].deiconify() if cat != '': self.cat_widgets[cat].show_feed(title) self.cat_widgets['All'].show_feed(title) self._feed_update(title) else: self.menu_feeds.disable_item(title) self.feed_widgets[title].withdraw() if cat != '': self.cat_widgets[cat].hide_feed(title) self.cat_widgets['All'].hide_feed(title) def feed_change_cat(self, title, old_cat, new_cat): if old_cat != new_cat: FEEDS.set(title, 'category', new_cat) if old_cat != '': self.cat_widgets[old_cat].remove_feed(title) if new_cat != '': if new_cat not in LATESTS.sections(): LATESTS.add_section(new_cat) LATESTS.set(new_cat, 'visible', 'True') LATESTS.set(new_cat, 'geometry', '') LATESTS.set(new_cat, 'position', 'normal') LATESTS.set(new_cat, 'sort_order', 'A-Z') self.cat_widgets[new_cat] = CatWidget(self, new_cat) self.cat_widgets[new_cat].event_generate('<Configure>') self.menu_categories.add_checkbutton( label=new_cat, command=lambda: self.toggle_category_widget(new_cat)) cst.add_trace(self.cat_widgets[new_cat].variable, 'write', lambda *args: self.cat_widget_trace(new_cat)) self.cat_widgets[new_cat].variable.set(True) else: try: filename = FEEDS.get(title, 'data') latest = cst.feed_get_latest(filename) except (configparser.NoOptionError, pickle.UnpicklingError): latest = '' self.cat_widgets[new_cat].entry_add( title, FEEDS.get(title, 'updated'), latest, FEEDS.get(title, 'url')) def feed_rename(self, old_name, new_name): options = { opt: FEEDS.get(old_name, opt) for opt in FEEDS.options(old_name) } FEEDS.remove_section(old_name) try: # check if feed's title already exists FEEDS.add_section(new_name) except configparser.DuplicateSectionError: i = 2 duplicate = True while duplicate: # increment i until new_name~#i does not already exist try: FEEDS.add_section("{}~#{}".format(new_name, i)) except configparser.DuplicateSectionError: i += 1 else: duplicate = False name = "{}~#{}".format(new_name, i) else: name = new_name logging.info("Renamed feed '%s' to '%s'", old_name, name) for opt, val in options.items(): FEEDS.set(name, opt, val) self._check_result_init_id[name] = self._check_result_init_id.pop( old_name, '') self._check_result_update_id[name] = self._check_result_update_id.pop( old_name, '') self.threads[name] = self.threads.pop(old_name, None) self.queues[name] = self.queues.pop(old_name) self.feed_widgets[name] = self.feed_widgets.pop(old_name) self.feed_widgets[name].rename_feed(name) self.cat_widgets['All'].rename_feed(old_name, name) category = FEEDS.get(name, 'category', fallback='') if category != '': self.cat_widgets[category].rename_feed(old_name, name) self.menu_feeds.delete(old_name) self.menu_feeds.add_checkbutton( label=name, command=lambda: self.toggle_feed_widget(name)) trace_info = cst.info_trace(self.feed_widgets[name].variable) if trace_info: cst.remove_trace(self.feed_widgets[name].variable, 'write', trace_info[0][1]) cst.add_trace(self.feed_widgets[name].variable, 'write', lambda *args: self.feed_widget_trace(name)) self.menu_feeds.set_item_value(name, self.feed_widgets[name].variable.get()) cst.save_feeds() return name def feed_remove(self, title): self.feed_widgets[title].destroy() del self.queues[title] try: del self.threads[title] except KeyError: pass del self.feed_widgets[title] try: del self._check_result_init_id[title] except KeyError: pass try: del self._check_result_update_id[title] except KeyError: pass try: os.remove(os.path.join(cst.PATH_DATA, FEEDS.get(title, 'data'))) except FileNotFoundError: pass self.menu_feeds.delete(title) logging.info("Removed feed '%s' %s", title, FEEDS.get(title, 'url')) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].remove_feed(title) if category != '': self.cat_widgets[category].remove_feed(title) FEEDS.remove_section(title) def feed_manage(self): dialog = Manager(self) self.wait_window(dialog) self.update_idletasks() cst.save_latests() if dialog.change_made: cst.save_feeds() self.feed_update() def feed_init(self): """Update feeds.""" for title in FEEDS.sections(): if FEEDS.getboolean(title, 'active', fallback=True): logging.info("Updating feed '%s'", title) self.threads[title] = Process(target=self.feed_get_info, args=(FEEDS.get(title, 'url'), self.queues[title], 'all'), daemon=True) self.threads[title].start() self._check_result_init(title) self._check_end_update_id = self.after(2000, self._check_end_update) def _check_result_init(self, title): if self.threads[title].is_alive(): self._check_result_init_id[title] = self.after( 1000, self._check_result_init, title) else: t, latest, updated, data = self.queues[title].get() if not t: if cst.internet_on(): run([ "notify-send", "-i", "dialog-error", _("Error"), _('{url} is not a valid feed.').format( url=FEEDS.get(title, 'url')) ]) logging.error('%s is not a valid feed.', FEEDS.get(title, 'url')) else: if self._notify_no_internet: run([ "notify-send", "-i", "dialog-error", _("Error"), _('No Internet connection.') ]) logging.warning('No Internet connection') self._notify_no_internet = False self._internet_id = self.after(30000, self.test_connection) after_ids = [ self._update_id, self._check_add_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass else: date = datetime.strptime(updated, '%Y-%m-%d %H:%M') if (date > datetime.strptime(FEEDS.get(title, 'updated'), '%Y-%m-%d %H:%M') or not FEEDS.has_option(title, 'data')): if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, title, cst.html2text(latest) ]) FEEDS.set(title, 'updated', updated) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].update_display( title, latest, updated) if category != '': self.cat_widgets[category].update_display( title, latest, updated) logging.info("Updated feed '%s'", title) self.feed_widgets[title].clear() for entry_title, date, summary, link in data: self.feed_widgets[title].entry_add( entry_title, date, summary, link, -1) logging.info("Populated widget for feed '%s'", title) self.feed_widgets[title].event_generate('<Configure>') self.feed_widgets[title].sort_by_date() try: filename = FEEDS.get(title, 'data') except configparser.NoOptionError: filename = cst.new_data_file() FEEDS.set(title, 'data', filename) cst.save_feeds() cst.save_data(filename, latest, data) else: logging.info("Feed '%s' is up-to-date", title) def _feed_update(self, title): """Update feed with given title.""" logging.info("Updating feed '%s'", title) self.threads[title] = Process(target=self.feed_get_info, args=(FEEDS.get(title, 'url'), self.queues[title]), daemon=True) self.threads[title].start() self._check_result_update(title) def feed_update(self): """Update all feeds.""" try: self.after_cancel(self._update_id) except ValueError: pass for thread in self.threads.values(): try: thread.terminate() except AttributeError: pass self.threads.clear() for title in FEEDS.sections(): if FEEDS.getboolean(title, 'active', fallback=True): self._feed_update(title) self._check_end_update_id = self.after(2000, self._check_end_update) def _check_result_update(self, title): if self.threads[title].is_alive(): self._check_result_update_id[title] = self.after( 1000, self._check_result_update, title) else: t, latest, updated, entry_title, summary, link = self.queues[ title].get(False) if not t: if cst.internet_on(): run([ "notify-send", "-i", "dialog-error", _("Error"), _('{url} is not a valid feed.').format( url=FEEDS.get(title, 'url')) ]) logging.error('%s is not a valid feed.', FEEDS.get(title, 'url')) else: if self._notify_no_internet: run([ "notify-send", "-i", "dialog-error", _("Error"), _('No Internet connection.') ]) logging.warning('No Internet connection') self._notify_no_internet = False self._internet_id = self.after(30000, self.test_connection) after_ids = [ self._update_id, self._check_add_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass else: date = datetime.strptime(updated, '%Y-%m-%d %H:%M') if date > datetime.strptime(FEEDS.get(title, 'updated'), '%Y-%m-%d %H:%M'): logging.info("Updated feed '%s'", title) if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, title, cst.html2text(latest) ]) FEEDS.set(title, 'updated', updated) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].update_display( title, latest, updated) if category != '': self.cat_widgets[category].update_display( title, latest, updated) self.feed_widgets[title].entry_add(entry_title, updated, summary, link, 0) self.feed_widgets[title].sort_by_date() try: filename = FEEDS.get(title, 'data') old, data = cst.load_data(filename) except pickle.UnpicklingError: cst.save_data(filename, latest, [(entry_title, updated, summary, link)]) except configparser.NoOptionError: filename = cst.new_data_file() FEEDS.set(title, 'data', filename) cst.save_data(filename, latest, [(entry_title, updated, summary, link)]) else: data.insert(0, (entry_title, updated, summary, link)) cst.save_data(filename, latest, data) else: logging.info("Feed '%s' is up-to-date", title) def _check_end_update(self): b = [t.is_alive() for t in self.threads.values() if t is not None] if sum(b): self._check_end_update_id = self.after(1000, self._check_end_update) else: cst.save_feeds() for widget in self.cat_widgets.values(): widget.sort() self._update_id = self.after( CONFIG.getint("General", "update_delay"), self.feed_update)