Exemple #1
1
 def _create_file_menu(self, master: Menu) -> Menu:
     menu = Menu(master, tearoff=0)
     menu.add_command(label="New",
                      command=self.new,
                      underline=0,
                      accelerator=_KEYS["new"][0])
     self.master.bind_all(_KEYS["new"][1], lambda event: self.new())
     menu.add_command(label="Open",
                      command=self.open,
                      underline=0,
                      accelerator=_KEYS["open"][0])
     self.master.bind_all(_KEYS["open"][1], lambda event: self.open())
     menu.add_command(label="Save",
                      command=self.save,
                      underline=0,
                      accelerator=_KEYS["save"][0])
     self.master.bind_all(_KEYS["save"][1], lambda event: self.save())
     menu.entryconfigure(2, state=DISABLED)
     self._document.bind("<<Modified>>",
                         lambda event: menu.entryconfigure(
                             2, state=_to_state(self._document.modified)),
                         add=True)
     menu.add_command(label="Save As", command=self.save_as, underline=5)
     menu.add_command(label="Close",
                      command=self.close,
                      underline=0,
                      accelerator=_KEYS["close"][0])
     self.master.bind_all(_KEYS["close"][1], lambda event: self.close())
     return menu
Exemple #2
0
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")
Exemple #3
0
class Program(Tk):
    """
    Class for application that takes a network in extended newick format and displays trees.
    This program can save the network and trees as images or text file.
    """
    def __init__(self):
        super().__init__()
        ORIGINAL_DPI = 96.0
        #Scale the window depending on current monitor's dpi
        self.current_dpi = self._get_dpi()
        self.scale = self.current_dpi / ORIGINAL_DPI
        self.tk.call("tk", "scaling", self.scale + 0.5)

        self.net_frame = None
        self.net_directory = ""
        self.trees_directory = ""
        self.save_directory = ""
        self.net_fig = None
        self.graph_window = None

        self.title("PhyloProgram")

        self.scaled_width = self._scale_window(750)
        self.scaled_height = self._scale_window(600)

        self.geometry(f"{self.scaled_width}x{self.scaled_height}")
        self.protocol("WM_DELETE_WINDOW", self._exit)

        #prompt windows
        self.input_prompt = None
        self.select_leaves_prompt = None

        self._initialise_menu_bar()
        self._initialise_tool_bar()
        self._initialise_info_bar()

        self.main_frame = Frame(self)
        self.main_frame.pack(side="top", fill="both", expand=1)

        #initialise network figure canvas in main window
        self.net_fig = plt.figure("Input network")
        self.net_fig.gca().clear()
        self.net_canvas = FigureCanvasTkAgg(self.net_fig,
                                            master=self.main_frame)

        self._initialise_main_text_widget()

        try:
            # PyInstaller creates a temp folder and stores path in _MEIPASS
            base_path = sys._MEIPASS
            print(f"PyInstaller temp folder: {base_path}")
            print(
                f"The PyInstaller temp folder can be deleted after session is closed\n"
            )

        except:
            pass

    def _get_dpi(self):
        """
        For private use. Get the dpi of the current screen
        
        Returns
        -------
        int
            DPI of current monitor
        """
        screen = Tk()
        current_dpi = screen.winfo_fpixels("1i")
        screen.destroy()
        return current_dpi

    def _initialise_menu_bar(self):
        """For private use. Initialise the top menu bar and bind shortcuts"""
        menu_bar = Menu(self)

        file_menu = Menu(menu_bar, tearoff=0)

        file_menu.add_command(label="Enter network",
                              command=self.new_network,
                              accelerator="Ctrl+N")
        file_menu.add_command(label="Open network...",
                              command=self.open_network,
                              accelerator="Ctrl+O")

        file_menu.add_separator()

        #drSPR sub menu
        rspr_graph_menu = Menu(file_menu, tearoff=0)
        file_menu.add_cascade(label="Create rSPR graph", menu=rspr_graph_menu)
        rspr_graph_menu.add_command(
            label="Enter trees",
            command=lambda: self.new_trees("Create rSPR graph"),
            accelerator="Ctrl+G")
        rspr_graph_menu.add_command(
            label="Open trees...",
            command=lambda: self.open_trees("Create rSPR graph"),
            accelerator="Ctrl+Shift+G")

        drspr_menu = Menu(file_menu, tearoff=0)
        file_menu.add_cascade(label="Calculate drSPR", menu=drspr_menu)
        drspr_menu.add_command(
            label="Enter trees",
            command=lambda: self.new_trees("Calculate drSPR"),
            accelerator="Ctrl+D")
        drspr_menu.add_command(
            label="Open trees...",
            command=lambda: self.open_trees("Calculate drSPR"),
            accelerator="Ctrl+Shift+D")

        file_menu.add_separator()

        self.save_sub_menu = Menu(file_menu, tearoff=0)
        self.save_sub_menu.add_command(label="Text file (trees only)",
                                       command=self.save_trees_only_text,
                                       accelerator="Ctrl+T")
        self.save_sub_menu.add_command(label="Text file",
                                       command=self.save_text,
                                       accelerator="Ctrl+Shift+T")
        self.save_sub_menu.add_command(label="Images",
                                       command=self.save_image,
                                       accelerator="Ctrl+Shift+I")
        file_menu.add_cascade(label="Save as...", menu=self.save_sub_menu)
        self.save_sub_menu.entryconfigure("Text file (trees only)",
                                          state="disabled")
        self.save_sub_menu.entryconfigure("Text file", state="disabled")
        self.save_sub_menu.entryconfigure("Images", state="disabled")
        self.text_save_enabled = False
        self.image_save_enabled = False

        file_menu.add_separator()
        file_menu.add_command(label="Exit", command=self._exit)
        menu_bar.add_cascade(label="File", menu=file_menu)

        help_menu = Menu(menu_bar, tearoff=0)
        help_menu.add_command(label="About", command=self.about)
        help_menu.add_command(label="Manual", command=self.manual)
        help_menu.add_command(label="More info", command=self.open_github)
        menu_bar.add_cascade(label="Help", menu=help_menu)

        self.config(menu=menu_bar)

        self.bind_all("<Control-n>", self.new_network)
        self.bind_all("<Control-o>", self.open_network)
        self.bind_all("<Control-g>",
                      lambda event: self.new_trees("Create rSPR graph"))
        self.bind_all("<Control-G>",
                      lambda event: self.open_trees("Create rSPR graph"))
        self.bind_all("<Control-d>",
                      lambda event: self.new_trees("Calculate drSPR"))
        self.bind_all("<Control-D>",
                      lambda event: self.open_trees("Calculate drSPR"))
        self.bind_all("<Control-T>", self.save_text)
        self.bind_all("<Control-t>", self.save_trees_only_text)
        self.bind_all("<Control-I>", self.save_image)

    def _initialise_tool_bar(self):
        """For private use. Initialise the tool bar"""
        self.toolbar = Frame(self, bd=1, relief="raised")
        self.toolbar.pack(fill="x")

        self.select_leaves_button = HoverButton(
            self.toolbar,
            text="Select leaves",
            relief="raised",
            command=self._select_leaves,
            state="disabled",
            tooltip_text=
            "Enter set leaves to be displayed in trees. By default, all labelled leaves in network are selected"
        )

        self.draw_button = HoverButton(
            self.toolbar,
            text="Draw trees/graph",
            relief="raised",
            command=self.generate_trees_graph,
            state="disabled",
            tooltip_text="Draw embedded/input trees or rSPR graph")

        self.graphics_enabled = IntVar()
        self.graphics = False
        graphics_check_box = Checkbutton(self.toolbar,
                                         text="Enable graphics",
                                         variable=self.graphics_enabled,
                                         command=self.set_graphics)
        graphics_check_box.pack(side="right", padx=(0, 10))
        ToolTip(
            graphics_check_box,
            "Enable graph visualisation for the next entered network/trees")

    def _initialise_info_bar(self):
        """For private use. Info bar that displays file opened, number of reticulations and labelled leaves in network"""
        self.info_frame = Frame(self)
        self.info_frame.pack(side="bottom", fill="x")

        self.info_label = Label(self.info_frame, text="")
        self.info_label.pack(side="right", padx=(0, 10))

        self.file_label = Label(self.info_frame, text="")
        self.file_label.pack(side="left", padx=(10, 0))

    def _update_info_bar(self, filename=""):
        """
        For private use. Update the network info when new network is opened.
        
        Parameters
        ----------
        filename : str, optional
            Name of network/trees file opened
        """
        if self.network:
            num_reticulations = self.network.num_reticulations
            num_labelled_leaves = self.network.num_labelled_leaves
            info_text = f"{num_reticulations} reticulations, {num_labelled_leaves} labelled leaves"
            self.info_label["text"] = info_text

        else:
            self.info_label["text"] = ""

        self.file_label["text"] = filename

    def _initialise_main_text_widget(self):
        """Setup the text widget in the main window"""
        self.main_text_widget = Text(self.main_frame, width=25)
        scroll = Scrollbar(self.main_text_widget,
                           command=self.main_text_widget.yview)
        self.main_text_widget['yscrollcommand'] = scroll.set
        scroll.pack(side="right", fill="y")

        #Bind to make text selectable on Mac
        self.main_text_widget.bind(
            "<1>", lambda event: self.main_text_widget.focus_set())

    def _enable_tree_tools(self):
        """For private use. Buttons involving trees are enabled when a network has successfully been processed and displayed."""
        self.select_leaves_button.config(state="normal")
        self.draw_button.config(state="normal")

    def _enable_tree_display(self):
        """For private use. Draw button enabled when graphics enabled"""
        self.select_leaves_button.config(state="disabled")
        self.draw_button.config(state="normal")

    def _disable_tree_tools(self):
        """For private use. Disable tree buttons in toolbar"""
        self.select_leaves_button.config(state="disabled")
        self.draw_button.config(state="disabled")

    def _enable_select_leaves(self):
        """For private use. Enable select leaves button and disable draw button in toolbar"""
        self.select_leaves_button.config(state="normal")
        self.draw_button.config(state="disabled")

    def _enable_save(self):
        """For private use. Save functions are enabled when a network and trees has successfully been processed and displayed."""
        self.text_save_enabled = True
        self.image_save_enabled = True
        self.save_sub_menu.entryconfigure("Text file", state="normal")
        self.save_sub_menu.entryconfigure("Images", state="normal")

        if self.network:
            self.save_sub_menu.entryconfigure("Text file (trees only)",
                                              state="normal")
        else:
            self.save_sub_menu.entryconfigure("Text file (trees only)",
                                              state="disabled")

    def _enable_text_save(self):
        """For private use. Used when visualisation is disabled. Only save as text enabled"""
        self.text_save_enabled = True
        self.image_save_enabled = False
        self.save_sub_menu.entryconfigure("Text file", state="normal")
        self.save_sub_menu.entryconfigure("Images", state="disabled")
        if self.network:
            self.save_sub_menu.entryconfigure("Text file (trees only)",
                                              state="normal")
        else:
            self.save_sub_menu.entryconfigure("Text file (trees only)",
                                              state="disabled")

    def _disable_save(self):
        self.text_save_enabled = False
        self.image_save_enabled = False
        self.save_sub_menu.entryconfigure("Text file", state="disabled")
        self.save_sub_menu.entryconfigure("Images", state="disabled")
        self.save_sub_menu.entryconfigure("Text file (trees only)",
                                          state="disabled")

    def _scale_window(self, window_length):
        """
        For private use. Scales the window based on calculated scale
        
        Parameters
        ----------
        window_length : int
            Window length dimension
            
        Returns
        -------
        int
            Scaled window length dimension
        """
        return round(window_length * self.scale)

    def _select_leaves(self):
        """For private use. Opens multiple choice dialog prompt to select the leaves that will be displayed in the generated trees."""
        if self.select_leaves_prompt:
            self.select_leaves_prompt.update()
            self.select_leaves_prompt.deiconify()
        else:
            self.select_leaves_prompt = MultiChoicePrompt(
                self,
                "Select leaves",
                "Specify the leaves that will be included in the generated trees.",
                ["All"],
                customise_option=True,
                text_placeholder="e.g. 1, 2, 3, 4")

    def set_graphics(self, *_):
        """Set if the program draws the network and trees."""
        if self.graphics_enabled.get() == 1:
            self.graphics = True
        else:
            self.graphics = False

    def about(self):
        """Display overview of program in window"""
        self.about_window = Window(title="About")
        path_file = path.resource_path("about.txt")
        print(f"\nOpened file at {path_file}\n")
        f = open(path_file, "r")
        about_text = f.read()
        text_widget = Text(self.about_window)
        text_widget.insert("1.0", about_text)
        text_widget.pack(expand=True, fill="both")
        text_widget.config(state="disabled")

    def manual(self):
        """Display program manual in window"""
        self.manual_window = Window(title="Manual",
                                    width=self.scaled_width,
                                    height=self.scaled_height // 2)
        path_file = path.resource_path("manual.txt")
        print(f"\nOpened file at {path_file}\n")
        f = open(path_file, "r")
        manual_text = f.read()
        text_widget = Text(self.manual_window, width=30)
        text_widget.insert("1.0", manual_text)
        scroll = Scrollbar(text_widget, command=text_widget.yview)
        text_widget['yscrollcommand'] = scroll.set
        scroll.pack(side="right", fill="y")
        text_widget.pack(expand=True, fill="both")
        text_widget.config(state="disabled")

    def open_github(self):
        """Open this program's Github page."""
        webbrowser.open("https://github.com/jaunzo/summer-research", new=2)

    def new_network(self, *_):
        """Displays dialog and gets network in extended newick format inputted by the user."""
        self.operation = "Network"

        if self.input_prompt:
            self.input_prompt.change_contents(
                "Enter network", "Enter network in extended newick format",
                "e.g. ((a,(b)#H1), (#H1,c));")
            self.input_prompt.update()
            self.input_prompt.deiconify()
        else:
            self.input_prompt = StringInputPrompt(
                self, "Enter network",
                "Enter network in extended newick format",
                "e.g. ((a,(b)#H1), (#H1,c));", self.operation)

    def open_network(self, *_):
        """Displays open file prompt and processes a text file that contains the network in extended newick format."""
        self.operation = "Network"

        filename = tkinter.filedialog.askopenfilename(
            initialdir=self.net_directory,
            title="Open network...",
            filetypes=(("text files", "*.txt"), ("all files", "*.*")))

        path = os.path.split(filename)
        self.net_directory = path[0]
        text_file = path[1]

        if filename != "":
            f = open(filename, "r")
            text = f.read().strip()

            if text != None:
                network_newick = text[:text.find(";") + 1]
                try:
                    self.generate_network(network_newick, text_file)
                except MalformedNewickException:
                    error_message = "Could not read network.\n\nNetwork requirements:\nNetwork must contain at least one labelled leaf and string must terminate with semicolon."
                    tkinter.messagebox.showerror(title="Open network error",
                                                 message=error_message)

    def generate_network(self, net_newick, filename=""):
        """
        Generate the network object and display it depending on graphics mode
        
        Parameters
        ----------
        net_newick : str
            Network in extended newick format
            
        filename : str, optional
            Filename of network opened (default="")
        """
        self.network = Network(net_newick, self.net_fig, self.graphics)
        self._update_info_bar(filename)

        self.net_newick = net_newick
        self._disable_save()

        if self.graphics:
            self._enable_tree_tools()
            self.main_text_widget.pack_forget()
            self.net_canvas.get_tk_widget().pack(side="top",
                                                 fill="both",
                                                 expand=1)
            self.display_network()
        else:
            self._enable_select_leaves()
            self.net_canvas.get_tk_widget().pack_forget()
            self.main_text_widget.pack(expand=True, fill="both")
            self.print_network()

    def print_network(self):
        """Print out the network in main window. Hide tree window"""
        if self.graph_window:
            self.graph_window.destroy()
            self.graph_window = None

        self.main_text_widget.config(state="normal")
        self.main_text_widget.delete('1.0', "end")
        self.main_text_widget.insert("1.0", self.network.text)
        self.main_text_widget.insert(
            "end", "\n\nSelect leaves to generate embedded trees.")
        self.main_text_widget.config(state="disabled")

    def display_network(self):
        """Display input network in the main window."""

        if self.graph_window:
            self.graph_window.withdraw()

        self.net_fig.gca().clear()

        try:
            self.network.draw()
            self.net_canvas.draw()
        except (ValueError, ImportError) as e:
            if self.net_fig:
                self.net_fig.clear()
                self.net_canvas.get_tk_widget().pack_forget()
                self.main_text_widget.pack(expand=True, fill="both")

            error_message = f"Error: {e}\n\nTo draw networks and trees, Graphviz must be installed and it's executables must be in the system's PATH."
            tkinter.messagebox.showerror(title="Open network error",
                                         message=error_message)

            self.graphics_enabled.set(0)
            self.graphics = False
            self._disable_tree_tools()

            self.main_text_widget.config(state="normal")
            self.main_text_widget.delete('1.0', "end")
            self.main_text_widget.insert(
                "1.0",
                "Error drawing network.\n\nGraphviz must be installed and it's executables must be in the system's PATH.\n"
            )
            self.main_text_widget.insert(
                "end",
                "Check Github page for more information. \nGithub page can be accessed in Help -> More info.\n\n"
            )
            self.main_text_widget.insert(
                "end",
                "Please enter network again with graphics disabled or install Graphviz if you \nwould like to proceed with graph visualisation."
            )
            self.main_text_widget.config(state="disabled")

    def generate_trees_graph(self):
        """Generate the tree/graph object and display them depending on graphics mode."""
        #Get Trees object
        if self.network:
            self.graph_trees = self.network.process()

            if not self.graphics:
                self.print_trees()

        if self.graphics:
            try:
                self.graph_trees.draw()
                self.display_trees_graph()
            except (ValueError, ImportError) as e:
                error_message = f"Error: {e}\n\nTo draw networks and trees, Graphviz must be installed and it's executables must be in the system's PATH."
                tkinter.messagebox.showerror(title="Draw error",
                                             message=error_message)
                print(
                    " Draw error: Graphviz must be installed to be able to draw graphs\n"
                )

                self.graphics_enabled.set(0)
                self.graphics = False
                self._disable_tree_tools()

    def print_trees(self):
        """Displays trees generated as text in the main window"""
        self.main_text_widget.config(state="normal")
        self.main_text_widget.delete('1.0', "end")
        self.main_text_widget.insert("1.0", self.network.text)
        self.main_text_widget.insert("end", self.graph_trees.text)
        self.main_text_widget.config(state="disabled")

        self._enable_text_save()

    def display_trees_graph(self, **kwargs):
        """
        Displays trees in a window when user clicks "Draw graph/trees" or selects leaves. Only one trees window is
        displayed at a time
        """
        #self.graph_trees.draw()

        if self.graph_window:
            self.graph_window.deiconify()
            self.graph_window.replace_graph(self.graph_trees, self.operation)
        else:
            #Create window
            if self.operation == "Network":
                title = "Embedded trees"
            elif self.operation == "Create rSPR graph":
                title = "rSPR Graph"
            else:
                title = "Trees"

            self.graph_window = GraphWindow(self,
                                            self.graph_trees,
                                            width=self.scaled_width,
                                            height=self.scaled_height,
                                            title=title,
                                            operation=self.operation,
                                            **kwargs)

        self._enable_save()

    def new_trees(self, operation):
        """
        Displays dialog and gets at least 2 trees in newick format inputted by the user
        
        Parameters
        ----------
        operation : str
            Specifies whether program is running rspr graph or drspr
        """
        self.operation = operation

        if self.input_prompt:
            self.input_prompt.change_contents(
                f"{self.operation}: Enter trees",
                "Enter at least 2 trees in newick format",
                "e.g.\n(((1,2),3),4);\n(((1,4),2),3);")
            self.input_prompt.operation = operation
            self.input_prompt.update()
            self.input_prompt.deiconify()
        else:
            self.input_prompt = StringInputPrompt(
                self, f"{operation}: Enter trees",
                "Enter at least 2 trees in newick format",
                "e.g.\n(((1,2),3),4);\n(((1,4),2),3);", self.operation)

    def open_trees(self, operation):
        """
        Opens file that contains at least 2 trees in newick format
        
        Parameters
        ----------
        operation : str
            Specifies whether program is running rspr graph or drspr
        """
        self.operation = operation

        filename = tkinter.filedialog.askopenfilename(
            initialdir=self.trees_directory,
            title=f"{self.operation}: Open trees...",
            filetypes=(("text files", "*.txt"), ("all files", "*.*")))

        path = os.path.split(filename)
        self.trees_directory = path[0]
        text_file = path[1]

        if filename != "":
            f = open(filename, "r")
            text = f.read().strip()

            if text != None and self.operation == "Calculate drSPR":
                try:
                    self.get_drspr(text, text_file)
                except MalformedNewickException:
                    error_message = "Could not read trees.\n\nPlease enter at least 2 trees.\nTrees must contain at least one labelled leaf and trees must terminate with semicolon."
                    tkinter.messagebox.showerror(title="Open trees error",
                                                 message=error_message)
            elif text != None and self.operation == "Create rSPR graph":
                self.get_rspr_graph(text, text_file)

    def get_rspr_graph(self, input_trees_string, filename=""):
        """
        Get rspr graph
        
        Parameters
        ----------
        input_trees_string : str
            String containing at least 2 trees in newick format delimited by semicolon
            
        filename : str, optional
            Filename of trees text file opened (default="")
        """
        if self.graph_window:
            self.graph_window.withdraw()

        self.network = None

        self.graph_trees = RsprGraph(input_trees_string)

        self.net_canvas.get_tk_widget().pack_forget()
        self.main_text_widget.pack(expand=True, fill="both")
        self._update_info_bar(filename)
        self.print_rspr_graph()
        self._enable_text_save()

        if self.graphics:
            self._enable_tree_display()
        else:
            self._disable_tree_tools()

    def print_rspr_graph(self):
        """Print all trees and adjacency list"""
        self.main_text_widget.config(state="normal")
        self.main_text_widget.delete('1.0', "end")

        self.main_text_widget.insert("end", self.graph_trees.text)
        self.main_text_widget.config(state="disabled")

    def get_drspr(self, input_trees, filename=""):
        """
        Get the rspr distance
        
        Parameters
        ----------
        input_trees : str
            String containing at least 2 trees in newick format delimited by semicolon
            
        filename : str, optional
            Filename of trees text file opened (default="")
        """
        if self.graph_window:
            self.graph_window.withdraw()

        self.network = None
        input_trees = input_trees.translate(str.maketrans('', '', ' \n\t\r'))
        trees_array = input_trees.split(";")

        if not trees_array[-1]:
            trees_array.pop()

        (distances, clusters,
         self.graph_trees) = d.calculate_drspr(trees_array)

        self.net_canvas.get_tk_widget().pack_forget()
        self.main_text_widget.pack(expand=True, fill="both")
        self._update_info_bar(filename)
        self.print_drspr(self.graph_trees.trees, distances, clusters)
        self._enable_text_save()

        if self.graphics:
            self._enable_tree_display()
        else:
            self._disable_tree_tools()

    def print_drspr(self, trees_array, distances, clusters):
        """
        Output rspr distance information in the main window
        
        Parameters
        ----------
        trees_array : list[str, PhylogeneticNetwork]
            Array of str or PhylogeneticNetwork objects
            
        distances : list[str]
            Array of distances
            
        clusters : list[str]
            Array of clusters
        """
        self.main_text_widget.config(state="normal")
        self.main_text_widget.delete('1.0', "end")

        self.main_text_widget.insert("end", "TREES:\n")
        text = ""

        for tree in trees_array:
            if type(tree) != str:
                text += f"{tree.text}\n"
            else:
                text += f"{tree}\n"

        self.main_text_widget.insert("end", text)

        length = len(distances)

        if length == 1:
            self.main_text_widget.insert("end", f"\ndrSPR = {distances[0]}\n")
            self.main_text_widget.insert("end", f"Clusters: {clusters[0]}\n")

        else:

            #Printing matrix
            self.main_text_widget.insert("end", "\nDISTANCE MATRIX:\n")
            for i in range(length):
                self.main_text_widget.insert("end",
                                             f"{', '.join(distances[i])}\n")

            #Printing cluster
            self.main_text_widget.insert("end", "\n\nCLUSTERS:")
            for i in range(length - 1):
                self.main_text_widget.insert(
                    "end", f"\nClusters compared with t{i+1}:\n")
                for j in range(i + 1, len(clusters[i])):
                    self.main_text_widget.insert(
                        "end",
                        f"t{j+1} (drSPR = {distances[i][j]}): {' '.join(clusters[i][j])}\n"
                    )

        self.main_text_widget.config(state="disabled")

    def save_trees_only_text(self, *_):
        """Saves just the tree newick strings in text file"""
        if self.text_save_enabled and self.network:
            file_contents = ""

            for tree in self.graph_trees.data.keys():
                file_contents += f"{tree}\n"

            title = "Saving trees as text file"

            f = tkinter.filedialog.asksaveasfile(
                initialdir=self.save_directory,
                title=title,
                filetypes=[("Text file", "*.txt")],
                defaultextension=[("Text file", "*.txt")])

            if f:  #if dialog not closed with "cancel".
                print("\nSaving trees only in text file...")
                path = os.path.split(f.name)
                self.save_directory = path[0]

                f.write(file_contents)
                f.close()
                print(f" Text file saved at {f.name}\n")

    def save_text(self, *_):
        """Saves network and trees with any other information in newick format as a text file in the directory that the user specifies."""
        if self.text_save_enabled:
            if self.network:
                file_contents = self.network.text
                file_contents += self.graph_trees.text
                title = "Saving network, trees and other info as text file"
            else:
                file_contents = self.main_text_widget.get("1.0", "end")
                title = "Saving trees and other info as text file"

            f = tkinter.filedialog.asksaveasfile(
                initialdir=self.save_directory,
                title=title,
                filetypes=[("Text file", "*.txt")],
                defaultextension=[("Text file", "*.txt")])

            if f:  #if dialog not closed with "cancel".
                print("\nSaving as text file...")

                path = os.path.split(f.name)
                self.save_directory = path[0]

                f.write(file_contents)
                f.close()
                print(f" Text file saved. at {f.name}\n")

    def save_image(self, *_):
        """Saves all figures as a series of images in the directory that the user specifies."""
        if self.image_save_enabled:
            if self.operation == "Network":
                title = "Select folder to save network and tree images"
            elif self.operation == "Create rSPR graph":
                title = "Select folder to save graph"
            else:
                title = "Select folder to save tree images"

            directory = tkinter.filedialog.askdirectory(
                initialdir=self.save_directory, title=title)

            abs_path = os.path.dirname(__file__)
            export_path = abs_path + directory

            #Export network
            if directory:  #if dialog not closed with "cancel".
                image_dpi = self.current_dpi * 2
                print("\nSaving image(s)...")

                self.save_directory = export_path

                if self.network:
                    self.net_fig.savefig(export_path + "/network.png",
                                         dpi=image_dpi,
                                         format='png',
                                         bbox_inches='tight')

                if self.operation == "Create rSPR graph":
                    self.graph_trees.figures[0].savefig(
                        f"{abs_path}{directory}/rspr_graph.png",
                        dpi=image_dpi,
                        format='png',
                        bbox_inches='tight')

                else:
                    #Export trees
                    num_figures = len(self.graph_trees.figures)

                    #count = 1
                    for i, tree_fig in enumerate(self.graph_trees.figures,
                                                 start=1):
                        tree_fig.savefig(
                            f"{abs_path}{directory}/trees{str(i)}.png",
                            dpi=image_dpi,
                            format='png',
                            bbox_inches='tight')
                        #count += 1
                        print(
                            f'\r {round(i / num_figures * 100)}% complete: Saved {i} / {num_figures} trees',
                            end="\r",
                            flush=True)

                print(f" 100% complete: Image(s) saved at {export_path}.\n")

    def _exit(self):
        """Display prompt dialog when user exits application."""
        MsgBox = tk.messagebox.askquestion(
            "Exit Application",
            "Are you sure you want to exit the application?",
            icon="warning")
        if MsgBox == "yes":
            os._exit(0)
Exemple #4
0
class MyMenu():
    def __init__(self,
                 tkObj,
                 newcmd=None,
                 opencmd=None,
                 savecmd=None,
                 exportcmd=None,
                 logincmd=None):
        #Popupmenü Cut, Copy, Paste - gebunden an den rechten Mausbutton (press and release)
        tkObj.bind('<Button-3><ButtonRelease-3>', self._show_popup)
        self._make_popup_menu(tkObj)

        self.menubar = Menu(tkObj)

        # create a pulldown menu, and add it to the menu bar
        self.filemenu = Menu(self.menubar, tearoff=0)
        self.filemenu.add_command(label='Neu', command=newcmd)
        self.filemenu.add_command(label='Öffnen', command=opencmd)
        self.filemenu.add_command(label='Speichern', command=savecmd)
        self.filemenu.add_separator()
        self.filemenu.add_command(label='PDF-Export',
                                  command=exportcmd,
                                  background='dark red',
                                  foreground='white')
        self.filemenu.add_separator()
        self.filemenu.add_command(label='Exit',
                                  background='black',
                                  foreground='white',
                                  command=tkObj.destroy)
        self.menubar.add_cascade(label='Datei', menu=self.filemenu)

        self.editmenu = Menu(self.menubar, tearoff=0)
        #lambda definiert anonyme Funktionen
        #tkObj.focus_get() holt das Objekt, das den aktuellen Fokus hat.
        #In diesem Fokus wird der entsprechende event generiert.
        self.editmenu.add_command(
            label='Ausschneiden',
            accelerator="Ctrl+X",
            command=lambda: tkObj.focus_get().event_generate('<<Cut>>'))
        self.editmenu.add_command(
            label='Kopieren',
            accelerator="Ctrl+C",
            command=lambda: tkObj.focus_get().event_generate('<<Copy>>'))
        self.editmenu.add_command(
            label='Einfügen',
            accelerator="Ctrl+V",
            command=lambda: tkObj.focus_get().event_generate('<<Paste>>'))
        self.menubar.add_cascade(label='Bearbeiten', menu=self.editmenu)

        #self.loginmenu = Menu(self.menubar, tearoff=0)
        #self.loginmenu.add_command(label='Login Datenbank', command=logincmd)
        #self.menubar.add_cascade(label='Login', menu=self.loginmenu)

        self.helpmenu = Menu(self.menubar, tearoff=0)
        self.helpmenu.add_command(label='Readme', command=self._readme)
        self.helpmenu.add_separator()
        self.helpmenu.add_command(label='Über SOPGUI', command=self._about)
        self.menubar.add_cascade(label='Hilfe', menu=self.helpmenu)

        tkObj.config(menu=self.menubar)

    #------------------------------------
    def _make_popup_menu(self, tkobj):
        #global popup_menu
        self.popup_menu = Menu(tkobj, tearoff=0)
        self.popup_menu.add_command(label='Cut')
        self.popup_menu.add_command(label='Copy')
        self.popup_menu.add_command(label='Paste')

    def _show_popup(
        self, e
    ):  #e ist der event, der zurückgerufen wird, wenn der callback aufgerufen wird.
        w = e.widget  #Abfrage des Objekts
        self.popup_menu.entryconfigure(
            'Cut', command=lambda: w.event_generate('<<Cut>>'))
        self.popup_menu.entryconfigure(
            'Copy', command=lambda: w.event_generate('<<Copy>>'))
        self.popup_menu.entryconfigure(
            'Paste', command=lambda: w.event_generate('<<Paste>>'))
        self.popup_menu.tk.call(
            'tk_popup', self.popup_menu, e.x_root,
            e.y_root)  #ruft an der Mauszeigerposition das popup_menu auf

    #------------------------------------
    def _readme(self):
        rm = messagebox.askokcancel(
            'Bekanntmachung! ',
            'Möchten Sie das Readme lesen?\nDie Datei öffnet sich im Webbrowser.'
        )
        if rm == True:
            htmlfile = 'README.html'
            if os.name == 'nt':  #windows
                os.startfile(htmlfile)
            elif os.name == 'posix':  #unix/linux
                os.system('/usr/bin/xdg-open ' + htmlfile)
        else:
            pass

    #------------------------------------
    def _about(self):
        about = 'Entwickelt unter:\n\nLinux (64-bit)\nPython (Version 3.6.7)\nThonny (Version 3.1.2)\nTk (8.6.8)'
        messagebox.showinfo('Über SOPGUI ', message=about)
Exemple #5
0
class Sync(Tk):
    """FolderSync main window."""
    def __init__(self):
        Tk.__init__(self, className='FolderSync')
        self.title("FolderSync")
        self.geometry("%ix%i" %
                      (self.winfo_screenwidth(), self.winfo_screenheight()))
        self.protocol("WM_DELETE_WINDOW", self.quitter)
        self.icon = PhotoImage(master=self, file=IM_ICON)
        self.iconphoto(True, self.icon)
        self.rowconfigure(2, weight=1)
        self.columnconfigure(0, weight=1)

        # --- icons
        self.img_about = PhotoImage(master=self, file=IM_ABOUT)
        self.img_open = PhotoImage(master=self, file=IM_OPEN)
        self.img_plus = PhotoImage(master=self, file=IM_PLUS)
        self.img_moins = PhotoImage(master=self, file=IM_MOINS)
        self.img_sync = PhotoImage(master=self, file=IM_SYNC)
        self.img_prev = PhotoImage(master=self, file=IM_PREV)
        self.img_expand = PhotoImage(master=self, file=IM_EXPAND)
        self.img_collapse = PhotoImage(master=self, file=IM_COLLAPSE)

        self.original = ""
        self.sauvegarde = ""
        # list of files / folders to delete before starting the copy because
        # they are not of the same type on the original and the backup
        self.pb_chemins = []
        self.err_copie = False
        self.err_supp = False

        # --- init log files
        l = [f for f in listdir(PATH) if match(r"foldersync[0-9]+.pid", f)]
        nbs = []
        for f in l:
            with open(join(PATH, f)) as fich:
                old_pid = fich.read().strip()
            if exists("/proc/%s" % old_pid):
                nbs.append(int(search(r"[0-9]+", f).group()))
            else:
                remove(join(PATH, f))
        if not nbs:
            i = 0
        else:
            nbs.sort()
            i = 0
            while i in nbs:
                i += 1
        self.pidfile = PID_FILE % i
        open(self.pidfile, 'w').write(str(getpid()))

        self.log_copie = LOG_COPIE % i
        self.log_supp = LOG_SUPP % i

        self.logger_copie = setup_logger("copie", self.log_copie)
        self.logger_supp = setup_logger("supp", self.log_supp)
        date = datetime.now().strftime('%d/%m/%Y %H:%M')
        self.logger_copie.info("\n###  %s  ###\n" % date)
        self.logger_supp.info("\n###  %s  ###\n" % date)

        # --- filenames and extensions that will not be copied
        exclude_list = split(r'(?<!\\) ',
                             CONFIG.get("Defaults", "exclude_copie"))
        self.exclude_names = []
        self.exclude_ext = []
        for elt in exclude_list:
            if elt:
                if elt[:2] == "*.":
                    self.exclude_ext.append(elt[1:])
                else:
                    self.exclude_names.append(elt.replace("\ ", " "))

        # --- paths that will not be deleted
        self.exclude_path_supp = [
            ch.replace("\ ", " ") for ch in split(
                r'(?<!\\) ', CONFIG.get("Defaults", "exclude_supp")) if ch
        ]
        #        while "" in self.exclude_path_supp:
        #            self.exclude_path_supp.remove("")

        self.q_copie = Queue()
        self.q_supp = Queue()
        # True if a copy / deletion is running
        self.is_running_copie = False
        self.is_running_supp = False

        self.style = Style(self)
        self.style.theme_use("clam")
        self.style.configure("TProgressbar",
                             troughcolor='lightgray',
                             background='#387EF5',
                             lightcolor="#5D95F5",
                             darkcolor="#2758AB")
        self.style.map("TProgressbar",
                       lightcolor=[("disabled", "white")],
                       darkcolor=[("disabled", "gray")])
        self.style.configure("folder.TButton", padding=0)
        # --- menu
        self.menu = Menu(self, tearoff=False)
        self.configure(menu=self.menu)

        # -------- recents
        self.menu_recent = Menu(self.menu, tearoff=False)
        if RECENT:
            for ch_o, ch_s in RECENT:
                self.menu_recent.add_command(
                    label="%s -> %s" % (ch_o, ch_s),
                    command=lambda o=ch_o, s=ch_s: self.open(o, s))
        else:
            self.menu.entryconfigure(0, state="disabled")

        # -------- favorites
        self.menu_fav = Menu(self.menu, tearoff=False)
        self.menu_fav_del = Menu(self.menu_fav, tearoff=False)
        self.menu_fav.add_command(label=_("Add"),
                                  image=self.img_plus,
                                  compound="left",
                                  command=self.add_fav)
        self.menu_fav.add_cascade(label=_("Remove"),
                                  image=self.img_moins,
                                  compound="left",
                                  menu=self.menu_fav_del)
        for ch_o, ch_s in FAVORIS:
            label = "%s -> %s" % (ch_o, ch_s)
            self.menu_fav.add_command(
                label=label, command=lambda o=ch_o, s=ch_s: self.open(o, s))
            self.menu_fav_del.add_command(
                label=label, command=lambda nom=label: self.del_fav(nom))
        if not FAVORIS:
            self.menu_fav.entryconfigure(1, state="disabled")

        # -------- log files
        menu_log = Menu(self.menu, tearoff=False)
        menu_log.add_command(label=_("Copy"), command=self.open_log_copie)
        menu_log.add_command(label=_("Removal"),
                             command=self.open_log_suppression)

        # -------- settings
        menu_params = Menu(self.menu, tearoff=False)
        self.copy_links = BooleanVar(self,
                                     value=CONFIG.getboolean(
                                         "Defaults", "copy_links"))
        self.show_size = BooleanVar(self,
                                    value=CONFIG.getboolean(
                                        "Defaults", "show_size"))
        menu_params.add_checkbutton(label=_("Copy links"),
                                    variable=self.copy_links,
                                    command=self.toggle_copy_links)
        menu_params.add_checkbutton(label=_("Show total size"),
                                    variable=self.show_size,
                                    command=self.toggle_show_size)
        self.langue = StringVar(self, CONFIG.get("Defaults", "language"))
        menu_lang = Menu(menu_params, tearoff=False)
        menu_lang.add_radiobutton(label="English",
                                  value="en",
                                  variable=self.langue,
                                  command=self.change_language)
        menu_lang.add_radiobutton(label="Français",
                                  value="fr",
                                  variable=self.langue,
                                  command=self.change_language)
        menu_params.add_cascade(label=_("Language"), menu=menu_lang)
        menu_params.add_command(label=_("Exclude from copy"),
                                command=self.exclusion_copie)
        menu_params.add_command(label=_("Exclude from removal"),
                                command=self.exclusion_supp)

        self.menu.add_cascade(label=_("Recents"), menu=self.menu_recent)
        self.menu.add_cascade(label=_("Favorites"), menu=self.menu_fav)
        self.menu.add_cascade(label=_("Log"), menu=menu_log)
        self.menu.add_cascade(label=_("Settings"), menu=menu_params)
        self.menu.add_command(image=self.img_prev,
                              compound="center",
                              command=self.list_files_to_sync)
        self.menu.add_command(image=self.img_sync,
                              compound="center",
                              state="disabled",
                              command=self.synchronise)
        self.menu.add_command(image=self.img_about,
                              compound="center",
                              command=lambda: About(self))
        # --- tooltips
        wrapper = TooltipMenuWrapper(self.menu)
        wrapper.add_tooltip(4, _('Preview'))
        wrapper.add_tooltip(5, _('Sync'))
        wrapper.add_tooltip(6, _('About'))

        # --- path selection
        frame_paths = Frame(self)
        frame_paths.grid(row=0, sticky="ew", pady=(10, 0))
        frame_paths.columnconfigure(0, weight=1)
        frame_paths.columnconfigure(1, weight=1)
        f1 = Frame(frame_paths, height=26)
        f2 = Frame(frame_paths, height=26)
        f1.grid(row=0, column=0, sticky="ew")
        f2.grid(row=0, column=1, sticky="ew")
        f1.grid_propagate(False)
        f2.grid_propagate(False)
        f1.columnconfigure(1, weight=1)
        f2.columnconfigure(1, weight=1)

        # -------- path to original
        Label(f1, text=_("Original")).grid(row=0, column=0, padx=(10, 4))
        f11 = Frame(f1)
        f11.grid(row=0, column=1, sticky="nsew", padx=(4, 0))
        self.entry_orig = Entry(f11)
        self.entry_orig.place(x=1, y=0, bordermode='outside', relwidth=1)
        self.b_open_orig = Button(f1,
                                  image=self.img_open,
                                  style="folder.TButton",
                                  command=self.open_orig)
        self.b_open_orig.grid(row=0, column=2, padx=(0, 7))
        # -------- path to backup
        Label(f2, text=_("Backup")).grid(row=0, column=0, padx=(8, 4))
        f22 = Frame(f2)
        f22.grid(row=0, column=1, sticky="nsew", padx=(4, 0))
        self.entry_sauve = Entry(f22)
        self.entry_sauve.place(x=1, y=0, bordermode='outside', relwidth=1)
        self.b_open_sauve = Button(f2,
                                   image=self.img_open,
                                   width=2,
                                   style="folder.TButton",
                                   command=self.open_sauve)
        self.b_open_sauve.grid(row=0, column=5, padx=(0, 10))

        paned = PanedWindow(self, orient='horizontal')
        paned.grid(row=2, sticky="eswn")
        paned.rowconfigure(0, weight=1)
        paned.columnconfigure(1, weight=1)
        paned.columnconfigure(0, weight=1)

        # --- left side
        frame_left = Frame(paned)
        paned.add(frame_left, weight=1)
        frame_left.rowconfigure(3, weight=1)
        frame_left.columnconfigure(0, weight=1)

        # -------- files to copy
        f_left = Frame(frame_left)
        f_left.columnconfigure(2, weight=1)
        f_left.grid(row=2,
                    columnspan=2,
                    pady=(4, 2),
                    padx=(10, 4),
                    sticky="ew")

        Label(f_left, text=_("To copy")).grid(row=0, column=2)
        frame_copie = Frame(frame_left)
        frame_copie.rowconfigure(0, weight=1)
        frame_copie.columnconfigure(0, weight=1)
        frame_copie.grid(row=3,
                         column=0,
                         sticky="eswn",
                         columnspan=2,
                         pady=(2, 4),
                         padx=(10, 4))
        self.tree_copie = CheckboxTreeview(frame_copie,
                                           selectmode='none',
                                           show='tree')
        self.b_expand_copie = Button(f_left,
                                     image=self.img_expand,
                                     style="folder.TButton",
                                     command=self.tree_copie.expand_all)
        TooltipWrapper(self.b_expand_copie, text=_("Expand all"))
        self.b_expand_copie.grid(row=0, column=0)
        self.b_expand_copie.state(("disabled", ))
        self.b_collapse_copie = Button(f_left,
                                       image=self.img_collapse,
                                       style="folder.TButton",
                                       command=self.tree_copie.collapse_all)
        TooltipWrapper(self.b_collapse_copie, text=_("Collapse all"))
        self.b_collapse_copie.grid(row=0, column=1, padx=4)
        self.b_collapse_copie.state(("disabled", ))
        self.tree_copie.tag_configure("warning", foreground="red")
        self.tree_copie.tag_configure("link",
                                      font="tkDefaultFont 9 italic",
                                      foreground="blue")
        self.tree_copie.tag_bind("warning", "<Button-1>", self.show_warning)
        self.tree_copie.grid(row=0, column=0, sticky="eswn")
        self.scroll_y_copie = Scrollbar(frame_copie,
                                        orient="vertical",
                                        command=self.tree_copie.yview)
        self.scroll_y_copie.grid(row=0, column=1, sticky="ns")
        self.scroll_x_copie = Scrollbar(frame_copie,
                                        orient="horizontal",
                                        command=self.tree_copie.xview)
        self.scroll_x_copie.grid(row=1, column=0, sticky="ew")
        self.tree_copie.configure(yscrollcommand=self.scroll_y_copie.set,
                                  xscrollcommand=self.scroll_x_copie.set)
        self.pbar_copie = Progressbar(frame_left,
                                      orient="horizontal",
                                      mode="determinate")
        self.pbar_copie.grid(row=4,
                             columnspan=2,
                             sticky="ew",
                             padx=(10, 4),
                             pady=4)
        self.pbar_copie.state(("disabled", ))

        # --- right side
        frame_right = Frame(paned)
        paned.add(frame_right, weight=1)
        frame_right.rowconfigure(3, weight=1)
        frame_right.columnconfigure(0, weight=1)

        # -------- files to delete
        f_right = Frame(frame_right)
        f_right.columnconfigure(2, weight=1)
        f_right.grid(row=2,
                     columnspan=2,
                     pady=(4, 2),
                     padx=(4, 10),
                     sticky="ew")
        Label(f_right, text=_("To remove")).grid(row=0, column=2)
        frame_supp = Frame(frame_right)
        frame_supp.rowconfigure(0, weight=1)
        frame_supp.columnconfigure(0, weight=1)
        frame_supp.grid(row=3,
                        columnspan=2,
                        sticky="eswn",
                        pady=(2, 4),
                        padx=(4, 10))
        self.tree_supp = CheckboxTreeview(frame_supp,
                                          selectmode='none',
                                          show='tree')
        self.b_expand_supp = Button(f_right,
                                    image=self.img_expand,
                                    style="folder.TButton",
                                    command=self.tree_supp.expand_all)
        TooltipWrapper(self.b_expand_supp, text=_("Expand all"))
        self.b_expand_supp.grid(row=0, column=0)
        self.b_expand_supp.state(("disabled", ))
        self.b_collapse_supp = Button(f_right,
                                      image=self.img_collapse,
                                      style="folder.TButton",
                                      command=self.tree_supp.collapse_all)
        TooltipWrapper(self.b_collapse_supp, text=_("Collapse all"))
        self.b_collapse_supp.grid(row=0, column=1, padx=4)
        self.b_collapse_supp.state(("disabled", ))
        self.tree_supp.grid(row=0, column=0, sticky="eswn")
        self.scroll_y_supp = Scrollbar(frame_supp,
                                       orient="vertical",
                                       command=self.tree_supp.yview)
        self.scroll_y_supp.grid(row=0, column=1, sticky="ns")
        self.scroll_x_supp = Scrollbar(frame_supp,
                                       orient="horizontal",
                                       command=self.tree_supp.xview)
        self.scroll_x_supp.grid(row=1, column=0, sticky="ew")
        self.tree_supp.configure(yscrollcommand=self.scroll_y_supp.set,
                                 xscrollcommand=self.scroll_x_supp.set)
        self.pbar_supp = Progressbar(frame_right,
                                     orient="horizontal",
                                     mode="determinate")
        self.pbar_supp.grid(row=4,
                            columnspan=2,
                            sticky="ew",
                            padx=(4, 10),
                            pady=4)
        self.pbar_supp.state(("disabled", ))

        # --- bindings
        self.entry_orig.bind("<Key-Return>", self.list_files_to_sync)
        self.entry_sauve.bind("<Key-Return>", self.list_files_to_sync)

    def exclusion_supp(self):
        excl = ExclusionsSupp(self)
        self.wait_window(excl)
        # paths that will not be deleted
        self.exclude_path_supp = [
            ch.replace("\ ", " ") for ch in split(
                r'(?<!\\) ', CONFIG.get("Defaults", "exclude_supp")) if ch
        ]

    def exclusion_copie(self):
        excl = ExclusionsCopie(self)
        self.wait_window(excl)
        exclude_list = CONFIG.get("Defaults", "exclude_copie").split(" ")
        self.exclude_names = []
        self.exclude_ext = []
        for elt in exclude_list:
            if elt:
                if elt[:2] == "*.":
                    self.exclude_ext.append(elt[2:])
                else:
                    self.exclude_names.append(elt)

    def toggle_copy_links(self):
        CONFIG.set("Defaults", "copy_links", str(self.copy_links.get()))

    def toggle_show_size(self):
        CONFIG.set("Defaults", "show_size", str(self.show_size.get()))

    def open_log_copie(self):
        open_file(self.log_copie)

    def open_log_suppression(self):
        open_file(self.log_supp)

    def quitter(self):
        rep = True
        if self.is_running_copie or self.is_running_supp:
            rep = askokcancel(
                _("Confirmation"),
                _("A synchronization is ongoing, do you really want to quit?"),
                parent=self)
        if rep:
            self.destroy()

    def del_fav(self, nom):
        self.menu_fav.delete(nom)
        self.menu_fav_del.delete(nom)
        FAVORIS.remove(tuple(nom.split(" -> ")))
        save_config()
        if not FAVORIS:
            self.menu_fav.entryconfigure(1, state="disabled")

    def add_fav(self):
        sauvegarde = self.entry_sauve.get()
        original = self.entry_orig.get()
        if original != sauvegarde and original and sauvegarde:
            if exists(original) and exists(sauvegarde):
                if not (original, sauvegarde) in FAVORIS:
                    FAVORIS.append((original, sauvegarde))
                    save_config()
                    label = "%s -> %s" % (original, sauvegarde)
                    self.menu_fav.entryconfigure(1, state="normal")
                    self.menu_fav.add_command(label=label,
                                              command=lambda o=original, s=
                                              sauvegarde: self.open(o, s))
                    self.menu_fav_del.add_command(
                        label=label,
                        command=lambda nom=label: self.del_fav(nom))

    def open(self, ch_o, ch_s):
        self.entry_orig.delete(0, "end")
        self.entry_orig.insert(0, ch_o)
        self.entry_sauve.delete(0, "end")
        self.entry_sauve.insert(0, ch_s)
        self.list_files_to_sync()

    def open_sauve(self):
        sauvegarde = askdirectory(self.entry_sauve.get(), parent=self)
        if sauvegarde:
            self.entry_sauve.delete(0, "end")
            self.entry_sauve.insert(0, sauvegarde)

    def open_orig(self):
        original = askdirectory(self.entry_orig.get(), parent=self)
        if original:
            self.entry_orig.delete(0, "end")
            self.entry_orig.insert(0, original)

    def sync(self, original, sauvegarde):
        """ peuple tree_copie avec l'arborescence des fichiers d'original à copier
            vers sauvegarde et tree_supp avec celle des fichiers de sauvegarde à
            supprimer """
        errors = []
        copy_links = self.copy_links.get()
        excl_supp = [
            path for path in self.exclude_path_supp
            if commonpath([path, sauvegarde]) == sauvegarde
        ]

        def get_name(elt):
            return elt.name.lower()

        def lower(char):
            return char.lower()

        def arbo(tree, parent, n):
            """ affiche l'arborescence complète de parent et renvoie la longueur
                maximale des items (pour gérer la scrollbar horizontale) """
            m = 0
            try:
                with scandir(parent) as content:
                    l = sorted(content, key=get_name)
                for item in l:
                    chemin = item.path
                    nom = item.name
                    if item.is_symlink():
                        if copy_links:
                            tree.insert(parent,
                                        'end',
                                        chemin,
                                        text=nom,
                                        tags=("whole", "link"))
                            m = max(m, len(nom) * 9 + 20 * (n + 1))
                    elif ((nom not in self.exclude_names)
                          and (splitext(nom)[-1] not in self.exclude_ext)):
                        tree.insert(parent,
                                    'end',
                                    chemin,
                                    text=nom,
                                    tags=("whole", ))
                        m = max(m, len(nom) * 9 + 20 * (n + 1))
                        if item.is_dir():
                            m = max(m, arbo(tree, chemin, n + 1))
            except NotADirectoryError:
                pass
            except Exception as e:
                errors.append(str(e))
            return m

        def aux(orig, sauve, n, search_supp):
            m_copie = 0
            m_supp = 0
            try:
                lo = listdir(orig)
                ls = listdir(sauve)
            except Exception as e:
                errors.append(str(e))
                lo = []
                ls = []
            lo.sort(key=lambda x: x.lower())
            ls.sort(key=lambda x: x.lower())
            supp = False
            copie = False
            if search_supp:
                for item in ls:
                    chemin_s = join(sauve, item)
                    if chemin_s not in excl_supp and item not in lo:
                        supp = True
                        self.tree_supp.insert(sauve,
                                              'end',
                                              chemin_s,
                                              text=item,
                                              tags=("whole", ))
                        m_supp = max(m_supp, int(len(item) * 9 + 20 * (n + 1)),
                                     arbo(self.tree_supp, chemin_s, n + 1))

            for item in lo:
                chemin_o = join(orig, item)
                chemin_s = join(sauve, item)
                if ((item not in self.exclude_names)
                        and (splitext(item)[-1] not in self.exclude_ext)):
                    if item not in ls:
                        # le dossier / fichier n'est pas dans la sauvegarde
                        if islink(chemin_o):
                            if copy_links:
                                copie = True
                                self.tree_copie.insert(orig,
                                                       'end',
                                                       chemin_o,
                                                       text=item,
                                                       tags=("whole", "link"))
                                m_copie = max(
                                    m_copie,
                                    (int(len(item) * 9 + 20 * (n + 1))))
                        else:
                            copie = True
                            self.tree_copie.insert(orig,
                                                   'end',
                                                   chemin_o,
                                                   text=item,
                                                   tags=("whole", ))
                            m_copie = max(
                                m_copie, (int(len(item) * 9 + 20 * (n + 1))),
                                arbo(self.tree_copie, chemin_o, n + 1))
                    elif islink(chemin_o) and exists(chemin_o):
                        # checking the existence prevent from copying broken links
                        if copy_links:
                            if not islink(chemin_s):
                                self.pb_chemins.append(chemin_o)
                                tags = ("whole", "warning", "link")
                            else:
                                tags = ("whole", "link")
                            self.tree_copie.insert(orig,
                                                   'end',
                                                   chemin_o,
                                                   text=item,
                                                   tags=tags)
                            m_copie = max(m_copie,
                                          int(len(item) * 9 + 20 * (n + 1)))
                            copie = True
                    elif isfile(chemin_o):
                        # first check if chemin_s is also a file
                        if isfile(chemin_s):
                            if getmtime(chemin_o) // 60 > getmtime(
                                    chemin_s) // 60:
                                # le fichier f a été modifié depuis la dernière sauvegarde
                                copie = True
                                self.tree_copie.insert(orig,
                                                       'end',
                                                       chemin_o,
                                                       text=item,
                                                       tags=("whole", ))
                        else:
                            self.pb_chemins.append(chemin_o)
                            self.tree_copie.insert(orig,
                                                   'end',
                                                   chemin_o,
                                                   text=item,
                                                   tags=("whole", "warning"))
                    elif isdir(chemin_o):
                        # to avoid errors due to unrecognized item types (neither file nor folder nor link)
                        if isdir(chemin_s):
                            self.tree_copie.insert(orig,
                                                   'end',
                                                   chemin_o,
                                                   text=item)
                            self.tree_supp.insert(sauve,
                                                  'end',
                                                  chemin_s,
                                                  text=item)
                            c, s, mc, ms = aux(
                                chemin_o, chemin_s, n + 1, search_supp
                                and (chemin_s not in excl_supp))
                            supp = supp or s
                            copie = copie or c
                            if not c:
                                # nothing to copy
                                self.tree_copie.delete(chemin_o)
                            else:
                                m_copie = max(
                                    m_copie, mc,
                                    int(len(item) * 9 + 20 * (n + 1)))
                            if not s:
                                # nothing to delete
                                self.tree_supp.delete(chemin_s)
                            else:
                                m_supp = max(m_supp, ms,
                                             int(len(item) * 9 + 20 * (n + 1)))
                        else:
                            copie = True
                            self.pb_chemins.append(chemin_o)
                            self.tree_copie.insert(orig,
                                                   'end',
                                                   chemin_o,
                                                   text=item,
                                                   tags=("whole", "warning"))
                            m_copie = max(
                                m_copie, (int(len(item) * 9 + 20 * (n + 1))),
                                arbo(self.tree_copie, chemin_o, n + 1))
            return copie, supp, m_copie, m_supp

        self.tree_copie.insert("",
                               0,
                               original,
                               text=original,
                               tags=("checked", ),
                               open=True)
        self.tree_supp.insert("",
                              0,
                              sauvegarde,
                              text=sauvegarde,
                              tags=("checked", ),
                              open=True)
        c, s, mc, ms = aux(original, sauvegarde, 1, True)
        if not c:
            self.tree_copie.delete(original)
            self.tree_copie.column("#0", minwidth=0, width=0)
        else:
            mc = max(len(original) * 9 + 20, mc)
            self.tree_copie.column("#0", minwidth=mc, width=mc)
        if not s:
            self.tree_supp.delete(sauvegarde)
            self.tree_supp.column("#0", minwidth=0, width=0)
        else:
            ms = max(len(sauvegarde) * 9 + 20, mc)
            self.tree_supp.column("#0", minwidth=ms, width=ms)
        return errors

    def show_warning(self, event):
        if "disabled" not in self.b_open_orig.state():
            x, y = event.x, event.y
            elem = event.widget.identify("element", x, y)
            if elem == "padding":
                orig = self.tree_copie.identify_row(y)
                sauve = orig.replace(self.original, self.sauvegarde)
                showwarning(
                    _("Warning"),
                    _("%(original)s and %(backup)s are not of the same kind (folder/file/link)"
                      ) % {
                          'original': orig,
                          'backup': sauve
                      },
                    master=self)

    def list_files_to_sync(self, event=None):
        """Display in a treeview the file to copy and the one to delete."""
        self.pbar_copie.configure(value=0)
        self.pbar_supp.configure(value=0)
        self.sauvegarde = self.entry_sauve.get()
        self.original = self.entry_orig.get()
        if self.original != self.sauvegarde and self.original and self.sauvegarde:
            if exists(self.original) and exists(self.sauvegarde):
                o_s = (self.original, self.sauvegarde)
                if o_s in RECENT:
                    RECENT.remove(o_s)
                    self.menu_recent.delete("%s -> %s" % o_s)
                RECENT.insert(0, o_s)
                self.menu_recent.insert_command(
                    0,
                    label="%s -> %s" % o_s,
                    command=lambda o=self.original, s=self.sauvegarde: self.
                    open(o, s))
                if len(RECENT) == 10:
                    del (RECENT[-1])
                    self.menu_recent.delete(9)
                save_config()
                self.menu.entryconfigure(0, state="normal")
                self.configure(cursor="watch")
                self.toggle_state_gui()
                self.update_idletasks()
                self.update()
                self.efface_tree()
                err = self.sync(self.original, self.sauvegarde)
                self.configure(cursor="")
                self.toggle_state_gui()
                c = self.tree_copie.get_children("")
                s = self.tree_supp.get_children("")
                if not (c or s):
                    self.menu.entryconfigure(5, state="disabled")
                    self.b_collapse_copie.state(("disabled", ))
                    self.b_expand_copie.state(("disabled", ))
                    self.b_collapse_supp.state(("disabled", ))
                    self.b_expand_supp.state(("disabled", ))
                elif not c:
                    self.b_collapse_copie.state(("disabled", ))
                    self.b_expand_copie.state(("disabled", ))
                elif not s:
                    self.b_collapse_supp.state(("disabled", ))
                    self.b_expand_supp.state(("disabled", ))
                if err:
                    showerror(_("Errors"), "\n".join(err), master=self)
                notification_send(_("Scan is finished."))
                warnings = self.tree_copie.tag_has('warning')
                if warnings:
                    showwarning(
                        _("Warning"),
                        _("Some elements to copy (in red) are not of the same kind on the original and the backup."
                          ),
                        master=self)
            else:
                showerror(_("Error"), _("Invalid path!"), master=self)

    def efface_tree(self):
        """Clear both trees."""
        c = self.tree_copie.get_children("")
        for item in c:
            self.tree_copie.delete(item)
        s = self.tree_supp.get_children("")
        for item in s:
            self.tree_supp.delete(item)
        self.b_collapse_copie.state(("disabled", ))
        self.b_expand_copie.state(("disabled", ))
        self.b_collapse_supp.state(("disabled", ))
        self.b_expand_supp.state(("disabled", ))

    def toggle_state_gui(self):
        """Toggle the state (normal/disabled) of key elements of the GUI."""
        if "disabled" in self.b_open_orig.state():
            state = "!disabled"
            for i in range(7):
                self.menu.entryconfigure(i, state="normal")
        else:
            state = "disabled"
            for i in range(7):
                self.menu.entryconfigure(i, state="disabled")
        self.tree_copie.state((state, ))
        self.tree_supp.state((state, ))
        self.entry_orig.state((state, ))
        self.entry_sauve.state((state, ))
        self.b_expand_copie.state((state, ))
        self.b_collapse_copie.state((state, ))
        self.b_expand_supp.state((state, ))
        self.b_collapse_supp.state((state, ))
        self.b_open_orig.state((state, ))
        self.b_open_sauve.state((state, ))

    def update_pbar(self):
        """
        Dislay the progress of the copy and deletion and put the GUI back in
        normal state once both processes are done.
        """
        if not self.is_running_copie and not self.is_running_supp:
            notification_send(_("Sync is finished."))
            self.toggle_state_gui()
            self.pbar_copie.configure(value=self.pbar_copie.cget("maximum"))
            self.pbar_supp.configure(value=self.pbar_supp.cget("maximum"))
            self.menu.entryconfigure(5, state="disabled")
            self.configure(cursor="")
            self.efface_tree()
            msg = ""
            if self.err_copie:
                msg += _(
                    "There were errors during the copy, see %(file)s for more details.\n"
                ) % {
                    'file': self.log_copie
                }
            if self.err_supp:
                msg += _(
                    "There were errors during the removal, see %(file)s for more details.\n"
                ) % {
                    'file': self.log_supp
                }
            if msg:
                showerror(_("Error"), msg, master=self)
        else:
            if not self.q_copie.empty():
                self.pbar_copie.configure(value=self.q_copie.get())
            if not self.q_supp.empty():
                self.pbar_supp.configure(value=self.q_supp.get())
            self.update()
            self.after(50, self.update_pbar)

    @staticmethod
    def get_list(tree):
        """Return the list of files/folders to copy/delete (depending on the tree)."""
        selected = []

        def aux(item):
            tags = tree.item(item, "tags")
            if "checked" in tags and "whole" in tags:
                selected.append(item)
            elif "checked" in tags or "tristate" in tags:
                ch = tree.get_children(item)
                for c in ch:
                    aux(c)

        ch = tree.get_children("")
        for c in ch:
            aux(c)
        return selected

    def synchronise(self):
        """
        Display the list of files/folders that will be copied / deleted
        and launch the copy and deletion if the user validates the sync.
        """
        # get files to delete and folder to delete if they are empty
        a_supp = self.get_list(self.tree_supp)
        # get files to copy
        a_copier = self.get_list(self.tree_copie)
        a_supp_avant_cp = []
        for ch in self.pb_chemins:
            if ch in a_copier:
                a_supp_avant_cp.append(
                    ch.replace(self.original, self.sauvegarde))
        if a_supp or a_copier:
            Confirmation(self, a_copier, a_supp, a_supp_avant_cp,
                         self.original, self.sauvegarde, self.show_size.get())

    def copie_supp(self, a_copier, a_supp, a_supp_avant_cp):
        """Launch sync."""
        self.toggle_state_gui()
        self.configure(cursor="watch")
        self.update()
        self.pbar_copie.state(("!disabled", ))
        self.pbar_supp.state(("!disabled", ))
        nbtot_copie = len(a_copier) + len(a_supp_avant_cp)
        self.pbar_copie.configure(maximum=nbtot_copie, value=0)
        nbtot_supp = len(a_supp)
        self.pbar_supp.configure(maximum=nbtot_supp, value=0)
        self.is_running_copie = True
        self.is_running_supp = True
        process_copie = Thread(target=self.copie,
                               name="copie",
                               daemon=True,
                               args=(a_copier, a_supp_avant_cp))
        process_supp = Thread(target=self.supp,
                              daemon=True,
                              name="suppression",
                              args=(a_supp, ))
        process_copie.start()
        process_supp.start()
        self.pbar_copie.configure(value=0)
        self.pbar_supp.configure(value=0)
        self.update_pbar()

    def copie(self, a_copier, a_supp_avant_cp):
        """
        Copie tous les fichiers/dossiers de a_copier de original vers
        sauvegarde en utilisant la commande système cp. Les erreurs
        rencontrées au cours du processus sont inscrites dans
        ~/.foldersync/copie.log
        """
        self.err_copie = False
        orig = abspath(self.original) + "/"
        sauve = abspath(self.sauvegarde) + "/"
        chdir(orig)
        self.logger_copie.info(
            _("\n###### Copy: %(original)s -> %(backup)s\n") % {
                'original': self.original,
                'backup': self.sauvegarde
            })
        n = len(a_supp_avant_cp)
        self.logger_copie.info(_("Removal before copy:"))
        for i, ch in zip(range(1, n + 1), a_supp_avant_cp):
            self.logger_copie.info(ch)
            p_copie = run(["rm", "-r", ch], stderr=PIPE)
            self.q_copie.put(i)
            err = p_copie.stderr.decode()
            if err:
                self.err_copie = True
                self.logger_copie.error(err.strip())
        self.logger_copie.info(_("Copy:"))
        for i, ch in zip(range(n + 1, n + 1 + len(a_copier)), a_copier):
            ch_o = ch.replace(orig, "")
            self.logger_copie.info("%s -> %s" % (ch_o, sauve))
            p_copie = run(["cp", "-ra", "--parents", ch_o, sauve], stderr=PIPE)
            self.q_copie.put(i)
            err = p_copie.stderr.decode()
            if err:
                self.err_copie = True
                self.logger_copie.error(err.strip())
        self.is_running_copie = False

    def supp(self, a_supp):
        """
        Supprime tous les fichiers/dossiers de a_supp de original vers
        sauvegarde en utilisant la commande système rm. Les erreurs
        rencontrées au cours du processus sont inscrites dans
        ~/.foldersync/suppression.log.
        """
        self.err_supp = False
        self.logger_supp.info(
            _("\n###### Removal:  %(original)s -> %(backup)s\n") % {
                'original': self.original,
                'backup': self.sauvegarde
            })
        for i, ch in enumerate(a_supp):
            self.logger_supp.info(ch)
            p_supp = run(["rm", "-r", ch], stderr=PIPE)
            self.q_supp.put(i + 1)
            err = p_supp.stderr.decode()
            if err:
                self.logger_supp.error(err.strip())
                self.err_supp = True
        self.is_running_supp = False

    def unlink(self):
        """Unlink pidfile."""
        unlink(self.pidfile)

    def change_language(self):
        """Change app language."""
        CONFIG.set("Defaults", "language", self.langue.get())
        showinfo(
            _("Information"),
            _("The language setting will take effect after restarting the application"
              ))
Exemple #6
0
class UserInterface(UIfunctions):
    def __init__(self, filehandler, databasehandler, path=None):
        self.title = "OpenKeynote (BETA)"
        self._filehandler = filehandler
        self._databasehandler = databasehandler
        self.path = path
        self.itemlist = []
        self.root = Tk()
        self.previeweditem = ""
        self.editeditem = ""
        self.case_selected = IntVar()
        self.parentname = ""
        self.autosave = IntVar()
        self._html_path = None

        self._default_title = self.title
        self.main_window()
        self.tree_view()
        self.frame_vertical_bar()
        self.bindings_and_menu()
        self.frame_setup()

        self.update_status()
        self.root.mainloop()

    def main_window(self, *args):
        self.mainframe = Frame(self.root)
        self.mainframe.grid(column=0, row=0, sticky=E+W+N+S)
        self.bottomframe = Frame(self.root)
        self.bottomframe.grid(column=0, row=1, sticky=E+W)
        self.statusbar = Label(
            self.bottomframe, text=self._filehandler.statustext, anchor=W)
        self.statusbar.pack(fill=BOTH, padx=0, pady=0)
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        self.root.rowconfigure(1, pad=10)
        self.pw = PanedWindow(self.mainframe, orient=HORIZONTAL)
        self.pw.pack(fill=BOTH, expand=1)
        self.pane_left = Frame(self.root)
        self.pw.add(self.pane_left)
        self.pane_right = Frame(self.root)
        self.pw.add(self.pane_right)

        self.frame_left = Frame(self.pane_left)
        self.frame_left.pack(fill=BOTH, expand=1, padx=3, pady=3)
        self.frame_center = Frame(self.pane_right)
        self.frame_center.grid(row=0, column=0, sticky=W+N, rowspan=4)
        self.frame_right = Frame(self.pane_right)
        self.frame_right.grid(row=0, column=1, sticky=W+E+N+S, padx=3, pady=3)
        self.pane_right.columnconfigure(1, weight=1)
        self.pane_right.rowconfigure(0, weight=1)
        self.sf1 = Text(self.frame_left, height=1, width=25, borderwidth=1,
                        relief="solid", highlightthickness=0)
        self.sf1.insert(1.0, "TODO: Searchbar")
        self.sf1.grid(row=0, column=0, sticky=W+E+N+S, pady=5)
        self.sf1.config(state=DISABLED)
        self.cs = Button(self.frame_left, text="X", width=1)
        self.cs.grid(row=0, column=1)
        self.frame_left.columnconfigure(0, weight=1)

    def tree_view(self, *args):
        """
        Tree view
        """
        self.l1 = ttk.Treeview(self.frame_left, columns=["stuff"], show="tree")
        self.yscroll = Scrollbar(self.frame_left, orient=VERTICAL)
        self.yscroll.config(width=10)
        self.l1['yscrollcommand'] = self.yscroll.set
        self.yscroll['command'] = self.l1.yview
        self.l1.grid(row=1, column=0, columnspan=3,  padx=30,
                     pady=10, sticky=N+S+E+W)
        self.yscroll.grid(row=1, column=0, columnspan=3, sticky=N+S+E)
        self.l1.bind("<ButtonRelease-1>", self.change_selection)

        self.frame_left.rowconfigure(1, weight=1)

    def frame_vertical_bar(self, *args):
        self.vbs = []
        middlebuttons = ("New Item", "New Subitem", "Delete",
                         "Rename", "Change Parent","- Descriptions (BETA) -")
        middlefunctions = (
            lambda: self.add_item(parent=self.parentname),
            lambda: self.add_item(parent=self.previeweditem),
            lambda: self.delete_item_dialog(),
            self.rename_item_dialog,
            self.change_parent_dialog,
            lambda: self.description_window(database_rows=self._databasehandler.view())
            #lambda: self.save_keynote_to_database(title="title",keynote="KN10", entreprise="min entreprise", category="")
            #self.save_all_keynotes_to_database
            )

        for a, button_text in enumerate(middlebuttons):
            self.vbs.append(ttk.Button(self.frame_center, text=button_text))
            self.vbs[a].pack(fill=BOTH)
            self.vbs[a].config(command=middlefunctions[a], width=10)
        for x in [2, 3, 4]:
            self.vbs[x].config(state=DISABLED)
        self.tx1 = Label(self.frame_right, text="Preview", anchor=W)
        self.tx1.grid(row=0, column=0, columnspan=3, sticky=W+E)
        self.tx2 = Label(self.frame_right, text="Editing", anchor=W)
        self.tx2.grid(row=2, column=0, sticky=W+E)

        self.e1 = scrolledtext.ScrolledText(self.frame_right,
                                            fg="#555", font=("Courier", 13),
                                            padx=10, pady=10,
                                            highlightthickness=0,
                                            borderwidth=1, relief="solid")
        self.e1.grid(row=1, column=0, columnspan=3, sticky=N+S+E+W)
        # was Text before

        self.e2 = scrolledtext.ScrolledText(self.frame_right,
                                            font=("Courier", 13), borderwidth=1,
                                            relief="solid", padx=10, pady=10,
                                            highlightthickness=0)
        self.e2.grid(row=3, column=0, columnspan=3, sticky=E+W+S+N)
        # AUTOSAVE
        self.autosaveFrame = LabelFrame(self.frame_center, text=' Autosave ')
        self.autosaveFrame.pack(fill=BOTH)
        self.autosave.trace(
            'w', lambda *args: print(f"Autosave: {self.autosave.get()}"))
        self.autosaveCheck = Checkbutton(
            self.autosaveFrame, text="Enabled", variable=self.autosave, anchor=W)
        self.autosaveCheck.select()
        self.autosaveCheck.pack(fill=BOTH)
        self.labelsFrame = LabelFrame(self.frame_center, text=' Change Case ')
        self.labelsFrame.pack(fill=BOTH)

        # CASE BUTTONS
        self.case_radiobuttons = []
        self.case_selected.set(99)
        rbtns = ['No change', 'UPPERCASE', 'lowercase', 'First Letter']
        for a, button_text in enumerate(rbtns):
            self.case_radiobuttons.append(
                Radiobutton(self.labelsFrame, text=button_text,
                            variable=self.case_selected,
                            value=a, command=self.change_case, width=10, anchor=W))
            self.case_radiobuttons[a].grid(sticky="W", row=a)

    def change_case(self, *args):
        pass

    def bindings_and_menu(self, *args):
        """
        Main key bindings
        """
        if os.name == "nt":
            self.CTRL = "Control"
            self.MBTN = "3"
        else:
            self.CTRL = "Command"
            self.MBTN = "2"

        def bindings_key(event):
            if event.state == 8 or event.state == 12:
                return
            else:
                return("break")

        self.sf1.bind("<Tab>", lambda a: self.focus_on(target=self.l1))
        self.sf1.bind("<Shift-Tab>", lambda a: self.focus_on(target=self.vb2))
        self.e1.bind("<Key>", bindings_key)
        self.e1.bind("<Tab>", lambda a: self.focus_on(target=self.e2))
        self.e1.bind(
            "<Shift-Tab>", lambda a: self.focus_on(target=self.vbs[-1]))

        self.e2.bind("<Tab>", lambda a: self.focus_on(target=self.vb1))
        self.e2.bind("<Shift-Tab>", lambda a: self.focus_on(target=self.e1))

        self.vb1 = ttk.Button(self.frame_right, text="Edit")
        self.vb1.grid(row=2, column=1)
        self.vb1.config(command=self.edit_item)
        self.vb2 = ttk.Button(self.frame_right, text="Save")
        self.vb2.grid(row=2, column=2)
        self.vb2.config(command=self.saveitem)

        self.frame_right.rowconfigure(1, weight=1)
        self.frame_right.rowconfigure(3, weight=1)
        self.frame_right.columnconfigure(0, weight=1)

        self.menu = Menu(self.root)
        self.root.config(menu=self.menu)
        file = Menu(self.menu, tearoff=0)  # TODO is it a win thing?
        file.add_command(label='New File*', command=self.close_file)
        file.add_command(
            label='Open File...', accelerator=f"{self.CTRL}-o",
            command=self.open_file_dialog)
        file.add_command(label='Save File',
                         accelerator=f"{self.CTRL}-s", command=self.save_file)
        file.add_command(label='Save File As...',
                         command=self.save_file_dialog)
        file.add_command(label='Close file', command=self.close_file)
        file.add_command(
            label='Exit', accelerator=f"{self.CTRL}-q", command=self.client_exit)
        self.menu.add_cascade(label='File', menu=file)

        self.clickmenu = Menu(self.root, tearoff=0)
        self.clickmenu.add_command(label="Cut")
        self.clickmenu.add_command(label="Copy")
        self.clickmenu.add_command(label="Paste")
        self.root.bind_class(
            "Text", f"<Button-{self.MBTN}><ButtonRelease-{self.MBTN}>",
            lambda event=None: self.right_click_menu())

        menu_edit = Menu(self.menu, tearoff=0)
        menu_edit.add_command(label='Select All', accelerator=f"{self.CTRL}-a",
                              command=self.select_all)
        self.root.bind(f"<{self.CTRL}-a>", self.select_all)
        self.e1.bind(f"<{self.CTRL}-a>", self.select_all)

        self.e1.bind(f"<{self.CTRL}-c>", self.e1.event_generate("<<Copy>>"))
        self.e2.bind(f"<{self.CTRL}-a>", self.select_all)
        menu_edit.add_command(label='Cut', accelerator=f"{self.CTRL}-x",
                              command=lambda: self.root.event_generate("<<Cut>>"))
        menu_edit.add_command(label='Copy', accelerator=f"{self.CTRL}-c",
                              command=lambda: self.copy_text())
        menu_edit.add_command(label='Paste', accelerator=f"{self.CTRL}-v",
                              command=lambda: self.root.event_generate("<<Paste>>"))
        self.menu.add_cascade(label='Edit', menu=menu_edit)

        menu_help = Menu(self.menu, tearoff=0)
        menu_help.add_command(label='About', command=self.about)
        self.menu.add_cascade(label='Help', menu=menu_help)

        for i in "Up,Down,Right,Return,Left".split(","):
            self.root.bind("<"+i+">", self.change_selection)

        self.e1.bind("<F2>", self.rename_item_dialog)
        self.e1.bind(f"<{self.CTRL}-s>", None)
        self.e1.bind(f"<{self.CTRL}-o>", None)
        self.root.bind("<F2>", self.rename_item_dialog)
        self.root.bind(f"<{self.CTRL}-s>", self.save_file)
        self.root.bind(f"<{self.CTRL}-o>", self.open_file_dialog)

    def copy_text(self, event=None):
        w = self.root.focus_get()
        w.event_generate("<<Copy>>")

    def update_title(self, title=""):
        self.root.title(title)

    def frame_setup(self, *args):
        """
        Misc UI functions
        """

        # sharp fonts in high res (https://stackoverflow.com/questions/41315873/
        # attempting-to-resolve-blurred-tkinter-text-scaling-on-windows-10-high-dpi-disp)
        if os.name == "nt":
            # TODO
            # self.root.protocol("WM_DELETE_WINDOW", self.client_exit)
            from ctypes import windll, pointer, wintypes
            try:
                windll.shcore.SetProcessDpiAwareness(1)
            except Exception:
                pass  # this will fail on Windows Server and maybe early Windows
            # TODO: Put link to ico file on windows.
            try:
                iconpath = Path("icon.ico")
                self.root.iconbitmap(Path())
            except:
                print("error with icon")
            else:  # mac?
                self.root.createcommand('exit', self.client_exit)

        self.root.title(self.title)
        if self.path:
            self.open_file(path=self.path)

        """ TODO: ICON /Windows

        self.root.iconbitmap("/Users/msn/Dropbox/py/Git/OpenKeynote/images/ico.icns")

        img = Image(
            "photo", file="/Users/msn/Dropbox/py/Git/OpenKeynote/images/large.gif")
        self.root.iconphoto(True, img) # you may also want to try this.
        self.root.call('wm','iconphoto', self.root._w, img)
        """
        self.width = min(int(self.root.winfo_screenwidth()-500), 1500)
        self.height = int(self.root.winfo_screenheight()-500)
        self.root.winfo_width()
        self.root.winfo_height()
        self.x = (self.root.winfo_screenwidth() // 2) - (self.width // 2)
        # self.x = 0
        self.y = (self.root.winfo_screenheight() // 2) - (self.height // 2)
        # self.y = 50
        self.root.geometry(f"{self.width}x{self.height}+{self.x}+{self.y}")
        self.root.update()
        self.root.after(0, self.fixUI)

    def right_click_menu(self, event=None):
        x, y = self.root.winfo_pointerxy()
        w = self.root.winfo_containing(x, y)
        # https://stackoverflow.com/a/8476726/11514850
        # w = self.root
        self.clickmenu.entryconfigure("Cut",
                                      command=lambda: w.event_generate("<<Cut>>"))
        self.clickmenu.entryconfigure("Copy",
                                      command=lambda: w.event_generate("<<Copy>>"))
        self.clickmenu.entryconfigure("Paste",
                                      command=lambda: w.event_generate("<<Paste>>"))
        self.clickmenu.tk.call("tk_popup", self.clickmenu,
                               w.winfo_pointerx(), w.winfo_pointery())

    def update_status(self, event=None):
        """
        Set statusbar in bottom of the window
        """
        self.statusbar.config(text=self._filehandler.refresh_status())
        self.root.after(100, self.update_status)

    def client_exit(self, *args):
        answer = messagebox.askyesnocancel('quit?', 'Save file first?')
        if answer == True:
            self.save_file()
            sys.exit()
        if answer == None:
            return
        if answer == False:
            exit()
class NewImageWindow(Toplevel):
    def __init__(self, master = None, pathToImage = None, name=None, image=None): 
        super().__init__(master = master)

        if pathToImage is not None and image is None:
            self.pathToImage = pathToImage
            self.image = ImageSaved(pathToImage)
        elif image is not None:
            self.image = image

        self.manager = StateManager(self.image.cv2Image)
        self.set_images()
        self.set_geometry()
        self.set_basic(master, name)
        self.place_menu()
        self.manage_line_profile()
        self.bind_functions()

    # BASICS
    def create_another(self, master, pathToImage, name, image):
        NewImageWindow(master, pathToImage, name, image)

    def set_geometry(self):
        self.minsize(self.imageFromArray.width, self.imageFromArray.height)
        self.geometry('{}x{}'.format(self.imageFromArray.width, self.imageFromArray.height)) 
    
    def set_images(self):        
        self.imageFromArray = Image.fromarray(self.image.cv2Image)
        self.imageCopy = self.imageFromArray.copy()

    def set_basic(self, master, name):
        self.focus_force()
        self.master = master
        self.name = name 
        self.title(name) 
        self.imagePanel = Canvas(self)
        self.imagePanel.place(relwidth=1, relheight = 1, x=0, y=0)

        self.profileWindow = None
        self.histogramWindow = None
        self.lutWindow = None
        self.thresholdScaleWindow = None
        self.posterizeWindow = None
        self.neighborWindow = None
        NewTwoArgsWindow.images[self] = self.name
        for widget in self.master.winfo_children():
            if(type(widget) == NewTwoArgsWindow):
                widget.update_list()
                break

    
    def manage_line_profile(self):
        self.lineCoords = {"x":0,"y":0,"x2":0,"y2":0}
        self.line = None

    def bind_functions(self):
        #Zmienić resize'owanie, aktualnie nie pasuje do linii profilu
        self.bind('<Configure>', self.resize_img)
        self.bind('<Control-d>', lambda event: self.duplicate_window())
        self.bind('<Control-z>', lambda event: self.undo())
        self.bind('<Control-y>', lambda event: self.redo())
        self.bind('<Control-s>', lambda event: self.save())
        self.bind("<ButtonPress-3>", self.click)
        self.bind("<B3-Motion>", self.drag)
        self.protocol("WM_DELETE_WINDOW", lambda:self.report_close_to_windows())

    def undo(self):
        self.image.cv2Image = self.manager.undo()
        self.image.copy = self.image.cv2Image
        self.image.fill_histogram()
        self.update_visible_image()
        self.update_child_windows()

    def save(self):
        try:
            cv2.imwrite(self.pathToImage, self.image.cv2Image)
        except AttributeError:
            self.save_as()

    def save_as(self):
        try:
            ext = self.image.file_extension
        except AttributeError:
            ext = '.bmp'
        finally:
            dir = ''
            dir = filedialog.asksaveasfilename(
            initialfile='*'+ext,
            defaultextension=ext, 
            filetypes=[
                ('JPEG', '*.jpg'), 
                ('PNG', '*.png'), 
                ('BMP', '*.bmp'), 
                ('GIF', '*.gif'),
                ]
            )
            if dir != '':
                try:
                    
                    cv2.imwrite(dir, self.image.cv2Image)
                    self.pathToImage = dir
                    self.name = os.path.split(dir)[1]
                    self.title(self.name)
                except cv2.error:
                    messagebox.showerror("Błąd", "Nieoczekiwany błąd, sprawdź czy dobrze wprowadziłeś nazwę pliku")


    def redo(self):
        self.image.cv2Image = self.manager.redo()
        self.image.copy = self.image.cv2Image
        self.image.fill_histogram()
        self.update_visible_image()
        self.update_child_windows()

    def report_close_to_windows(self):
        NewTwoArgsWindow.images.pop(self)
        for widget in self.master.winfo_children():
            if(type(widget) == NewTwoArgsWindow):
                widget.update_list()
                break
        self.destroy()
    # -------------------

    # WINDOW
    def duplicate_window(self):
        NewImageWindow(self.master, None, self.name + '(Kopia)', ImageSaved(None,self.image.cv2Image)).focus_set()
    
    def place_menu(self):
        topMenu = Menu(self)
        self.dsc = Menu(topMenu, tearoff=False)
        histMan = Menu(topMenu, tearoff=False)
        
        imageMan = Menu(topMenu, tearoff=False)
        imageAnalize = Menu(topMenu, tearoff=False)
        pointOper = Menu(imageMan, tearoff=False)

        neighborOper = Menu(imageMan, tearoff=False)
        smooth = Menu(neighborOper, tearoff=False)
        detectEdges = Menu(neighborOper, tearoff=False)
        prewitt = Menu(neighborOper, tearoff=False)
        sharpen = Menu(neighborOper, tearoff=False)
        medianM = Menu(neighborOper, tearoff=False)


        imageMan.add_cascade(label="Jednoargumentowe", menu=pointOper)
        imageMan.add_command(label="Dwuargumentowe", compound=LEFT, command=self.create_two_args_window)
        imageMan.add_cascade(label="Sąsiedztwa", menu=neighborOper)
        imageMan.add_command(label="Morfologiczne", compound=LEFT, command=self.create_morph_window)
        imageMan.add_command(label="Morfologiczna ekstrakcja linii", compound=LEFT, command=self.create_morph_line_window)
        imageMan.add_command(label="Watershed", compound=LEFT, command=self.handle_watershed)
        
        # Opcje OPISU
        self.dsc.add_command(label='Cofnij', compound=LEFT, accelerator='Ctrl+Z', command=self.undo)
        self.dsc.add_command(label='Powtórz', compound=LEFT, accelerator='Ctrl+Y', command=self.redo)
        self.dsc.add_command(label='Zapisz', accelerator='Ctrl+S', compound=LEFT, command=self.save)
        self.dsc.add_command(label='Zapisz jako...', accelerator='Ctrl+Shift+S', compound=LEFT, command=self.save_as)
        self.dsc.add_command(label='Histogram', compound=LEFT, command=self.create_histogram_window)
        self.dsc.add_command(label='LUT', compound=LEFT, command=self.create_lut_window)        
        self.dsc.add_command(label='Linia profilu', compound=LEFT, state=DISABLED, command=self.create_profile_window)

        # Opcje MANIPULACJI HISTOGRAMEM
        histMan.add_command(label="Rozciąganie", compound=LEFT, command=self.stretch_image)
        histMan.add_command(label="Rozciąganie przedziałami", compound=LEFT, command=self.create_stretching_window)
        histMan.add_command(label="Wyrównanie", compound=LEFT, command=self.equalize_image)

        # Opcje OPERACJI JEDNOARGUMENTOWYCH
        pointOper.add_command(label="Negacja", compound=LEFT, command=self.negate_image)
        pointOper.add_command(label="Progowanie", compound=LEFT, command=lambda:self.threshold_image("BASIC"))
        pointOper.add_command(label="Progowanie adaptacyjne", compound=LEFT, command=lambda:self.threshold_image("ADAPT"))
        pointOper.add_command(label="Progowanie Otsu", compound=LEFT, command=lambda:self.threshold_image("OTSU"))
        pointOper.add_command(label="Posteryzacja", compound=LEFT, command=self.posterize_image)

        # Opcje OPERACJI SĄSIEDZTWA
        neighborOper.add_cascade(label="Wygładzanie", menu=smooth)
        neighborOper.add_cascade(label="Detekcja krawędzi", menu=detectEdges)
        neighborOper.add_cascade(label="Wyostrzanie", menu=sharpen)
        neighborOper.add_cascade(label="Medianowe", menu=medianM)
        neighborOper.add_command(label="Własne", compound=LEFT, command=lambda:self.create_custom_mask_window(1))
        neighborOper.add_command(label="Własne splot", compound=LEFT, command=lambda:self.create_custom_mask_window(0))

        neighborOper.add_separator()
        neighborOper.add_command(label="OPCJE SKRAJNYCH PIKSELI", state='disabled', compound=LEFT)
        self.border = IntVar(self)
        self.border.set(2)
        neighborOper.add_radiobutton(label="Bez zmian (isolated)", variable=self.border, value=0)
        neighborOper.add_radiobutton(label="Odbicie lustrzane (reflect)", variable=self.border, value=1)
        neighborOper.add_radiobutton(label="Powielenie skrajnego piksela (replicate)", variable=self.border, value=2)
        
        smooth.add_command(label="Blur", compound=LEFT, command=lambda:self.handle_neighbor_operations("BLUR", self.border.get())) #POZMIENIAĆ COMMANDY
        smooth.add_command(label="Gaussian Blur", compound=LEFT, command=lambda:self.handle_neighbor_operations("GAUSSIAN", self.border.get()))

        detectEdges.add_command(label="Sobel", compound=LEFT, command=lambda:self.handle_neighbor_operations("SOBEL", self.border.get()))
        detectEdges.add_command(label="Laplasjan", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN", self.border.get()))
        detectEdges.add_command(label="Canny", compound=LEFT, command=lambda:self.handle_neighbor_operations("CANNY", self.border.get()))
        detectEdges.add_cascade(label="Prewitt", menu=prewitt)
        
        prewitt.add_command(label="Prewitt N", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW N", self.border.get()))
        prewitt.add_command(label="Prewitt NE", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW NE", self.border.get()))
        prewitt.add_command(label="Prewitt E", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW E", self.border.get()))
        prewitt.add_command(label="Prewitt SE", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW SE", self.border.get()))
        prewitt.add_command(label="Prewitt S", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW S", self.border.get()))
        prewitt.add_command(label="Prewitt SW", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW SW", self.border.get()))
        prewitt.add_command(label="Prewitt W", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW W", self.border.get()))
        prewitt.add_command(label="Prewitt NW", compound=LEFT, command=lambda:self.handle_neighbor_operations("PRW NW", self.border.get()))

        sharpen.add_command(label="Laplasjan 0/-1/5", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN 1", self.border.get()))
        sharpen.add_command(label="Laplasjan -1/-1/9", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN 2", self.border.get()))
        sharpen.add_command(label="Laplasjan 1/-2/5", compound=LEFT, command=lambda:self.handle_neighbor_operations("LAPLASJAN 3", self.border.get()))

        medianM.add_command(label="Maska 3x3", compound=LEFT, command=lambda:self.handle_neighbor_operations("MEDIAN 3", self.border.get()))
        medianM.add_command(label="Maska 5x5", compound=LEFT, command=lambda:self.handle_neighbor_operations("MEDIAN 5", self.border.get()))
        medianM.add_command(label="Maska 7x7", compound=LEFT, command=lambda:self.handle_neighbor_operations("MEDIAN 7", self.border.get()))        

        # OPECJE ANALIZY
        imageAnalize.add_command(label="Wyznaczenie wektora cech obiektów", compound=LEFT, command=self.handle_objects_vector)
        imageAnalize.add_command(label="Klasyfikacja obiektów fasoli, ryżu i grochu", compound=LEFT, command=self.handle_classify)

        # DODANIE GŁÓWNYCH ZAKŁADEK
        topMenu.add_cascade(label="Obraz", menu=self.dsc)
        topMenu.add_cascade(label="Manipulacja histogramem", menu=histMan)
        topMenu.add_cascade(label="Operacje", menu=imageMan)
        topMenu.add_cascade(label="Analiza", menu=imageAnalize)

        self.config(menu = topMenu)
    
    def update_visible_image(self):
        self.set_images()
        self.photoImage = ImageTk.PhotoImage(self.imageFromArray)
        self.imagePanel.delete("all")
        self.imagePanel.create_image(0, 0, anchor=NW, image=self.photoImage)
    # -------------------

    # SET CHILD WINDOWS
    def create_moments_window(self, colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters):
        for widget in self.winfo_children():
            if(type(widget) == NewMomentsWindow):
                widget.lift()
                widget.focus_set()
                return
        wg = NewMomentsWindow(self, colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters)
        wg.focus_set()

    def create_morph_window(self):
        if not self.image.check_if_binary():
            messagebox.showerror("Błąd", "Obraz musi być binarny. Wykonaj posteryzację dla wartości 2 i spróbuj ponownie.")
            return
        for widget in self.winfo_children():
            if(type(widget) == NewMorphWindow):
                widget.lift()
                widget.focus_set()
                return
        wg = NewMorphWindow(self)
        wg.focus_set()

    def create_morph_line_window(self):
        for widget in self.winfo_children():
            if(type(widget) == NewMorphLineWindow):
                widget.lift()
                widget.focus_set()
                return
        wg = NewMorphLineWindow(self)
        wg.focus_set()

    def create_custom_mask_window(self, option):
        if option == 1:
            for widget in self.winfo_children():
                if(type(widget) == NewCustomMaskWindow):
                    widget.lift()
                    widget.focus_set()
                    return
            wg = NewCustomMaskWindow(self)
            wg.focus_set()
        elif option == 0:
            for widget in self.winfo_children():
                if(type(widget) == NewCustomMaskWindowConv):
                    widget.lift()
                    widget.focus_set()
                    return
            wg = NewCustomMaskWindowConv(self)
            wg.focus_set()

    def create_two_args_window(self):
        for widget in self.master.winfo_children():
            if(type(widget) == NewTwoArgsWindow):
                widget.lift()
                widget.focus_set()
                return
        wg = NewTwoArgsWindow(self.master)
        wg.focus_set()

    def create_profile_window(self):
        if(self.profileWindow is not None):
            self.profileWindow.lift()
            self.profileWindow.focus_set()
            return
        self.profileWindow = NewLineProfileWindow(self.name, self.lineCoords, self)
        
    def create_histogram_window(self):
        if(self.histogramWindow is not None):
            self.histogramWindow.lift()
            self.histogramWindow.focus_set()
            return
        self.histogramWindow = NewHistogramWindow(self.name, self)

    def create_stretching_window(self):
        for widget in self.winfo_children():
            if(type(widget) == NewCustomStretchWindow):
                widget.lift()
                widget.focus_set()
                return
        wg = NewCustomStretchWindow(self)
        wg.focus_set()

    def create_lut_window(self):
        if(self.lutWindow is not None):
            self.lutWindow.lift()
            self.lutWindow.focus_set()
            return
        self.lutWindow = NewLutWindow(self.name, self)

    def update_child_windows(self):
        if self.histogramWindow is not None:
            self.histogramWindow.update_histogram()

        if self.lutWindow is not None:
            self.lutWindow.display_lut_values()

        if self.profileWindow is not None:
            self.profileWindow.update_line(self.lineCoords)

    def resize_child_windows(self):
        offsetX = self.winfo_rootx()-self.winfo_x()
        offsetY = self.winfo_rooty()-self.winfo_y()
        if self.posterizeWindow is not None:
            self.posterizeWindow.geometry('%dx%d+%d+%d' % (self.winfo_width(), self.posterizeWindow.height, self.winfo_x()+offsetX, self.winfo_y()+self.winfo_height()+offsetY+2))
        if self.thresholdScaleWindow is not None:
            self.thresholdScaleWindow.geometry('%dx%d+%d+%d' % (self.thresholdScaleWindow.width, self.winfo_height(),self.winfo_x()+self.winfo_width()+offsetX+2, self.winfo_y()+offsetY))
    # -------------------

    # ONE CLICK OPERATIONS
    def handle_classify(self):
        self.image.classify()
        self.manager.new_state(self.image.cv2Image)
        self.update_visible_image()
        self.update_child_windows()
        messagebox.showinfo("LEGENDA", "Zielony = ryz \n Niebieski = soczewica \n Czerwony = fasola")

    def handle_objects_vector(self):
        colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters = self.image.get_objects_vector()
        self.manager.new_state(self.image.cv2Image)
        self.update_visible_image()
        self.update_child_windows()
        self.create_moments_window(colours, moments, areas, lengths, aspectRatios, extents, solidities, equivalentDiameters)

    def handle_watershed(self):
        self.image.my_watershed()
        self.image.copy = self.image.cv2Image
        self.manager.new_state(self.image.cv2Image)
        self.update_visible_image()
        self.update_child_windows()

    def equalize_image(self):
        self.image.equalize()
        self.manager.new_state(self.image.cv2Image)
        self.update_visible_image()
        self.update_child_windows()

    def stretch_image(self, oldMin=None, oldMax=None, newMini=None, newMaxi=None):
        self.image.stretch(oldMin, oldMax, newMini, newMaxi)
        self.manager.new_state(self.image.cv2Image)
        self.update_visible_image()
        self.update_child_windows()

    def negate_image(self):
        self.image.negate()
        self.manager.new_state(self.image.cv2Image)
        self.update_visible_image()
        self.update_child_windows()

    def threshold_image(self, method):
        if method == "BASIC":
            if self.thresholdScaleWindow is None:
                self.thresholdScaleWindow = NewSliderWindow(self)
            else:
                self.thresholdScaleWindow.cancel()
                self.thresholdScaleWindow = NewSliderWindow(self)
        elif method == "ADAPT":
            if self.thresholdScaleWindow is None:
                self.thresholdScaleWindow = NewAdaptiveSliderWindow(self)
            else:
                self.thresholdScaleWindow.cancel()
                self.thresholdScaleWindow = NewAdaptiveSliderWindow(self)
        elif method == "OTSU":
            self.image.threshold_otsu()
            self.manager.new_state(self.image.cv2Image)
            self.update_visible_image()
            self.update_child_windows()

    def posterize_image(self):
        if self.posterizeWindow is None:
            self.posterizeWindow = NewPosterizeWindow(self.name, self)
            self.manager.new_state(self.image.cv2Image)
            self.update_visible_image()
            self.update_child_windows()

    def handle_neighbor_operations(self, operation, borderOption):
        self.image.neighbor_operations(operation, borderOption)
        self.manager.new_state(self.image.cv2Image)
        self.update_visible_image()
        self.update_child_windows()
    # -------------------


    # ON EVENT
    def click(self, e):
        self.lineCoords["x"] = e.x
        self.lineCoords["y"] = e.y
        self.dsc.entryconfigure('Linia profilu', state=NORMAL)

    def drag(self, e):
        self.lineCoords["x2"] = e.x
        self.lineCoords["y2"] = e.y
        if self.line is not None:
            self.imagePanel.delete(self.line) 
        self.line = self.imagePanel.create_line(self.lineCoords["x"], self.lineCoords["y"], self.lineCoords['x2'], self.lineCoords['y2'], fill='red', dash=(2, 2))
        
        if self.profileWindow is not None:
            self.profileWindow.update_line(self.lineCoords)

    def resize_img(self, event):
        self.resize_child_windows()
        #self.newWidth = event.width
        #self.newHeight = event.height
        #self.imageFromArray = self.imageCopy.resize((self.newWidth, self.newHeight))
        self.photoImage = ImageTk.PhotoImage(self.imageFromArray)
        self.imagePanel.create_image(0, 0, anchor=NW, image=self.photoImage)
    # -------------------
Exemple #8
0
class Application:
    def __init__(self):
        self.root = Tk()
        VARIABLES.set_master(self.root)
        self.root.wm_title('asterios')
        self._init_menu()

        paned_window = PanedWindow(self.root, orient='vertical')

        nb = Notebook(paned_window)

        self.tips_text = Text(nb)
        nb.add(self.tips_text, text='tips')

        self.puzzle_text = PuzzleViewer(nb)
        nb.add(self.puzzle_text, text='puzzle')

        self.solver_text = CodeEditor(nb)
        nb.add(self.solver_text, text='solver')
        self.solver_text.insert(
            'end',
            textwrap.dedent('''\
                def solve(puzzle):
                    """
                    Complete cette fonction
                    """
                    puzzle_solved = '...'

                    return puzzle_solved
            ''')
        )

        self.notif_text = NotificationViewer(paned_window, relief='ridge', border=3)

        paned_window.add(nb, sticky='nsew')
        paned_window.add(self.notif_text, sticky='nsew', minsize=80)
        paned_window.pack(fill='both', expand=1)
        self.show_configuration_window()

    def _init_menu(self):
        self.menubar = Menu(self.root)
        self.root.config(menu=self.menubar)
        self.menubar.insert_command(1, label="config",
                                    command=self.show_configuration_window)
        self.menubar.insert_command(2, label="run", command=self.solve)

    def show_configuration_window(self):
        Configurator(self, start_command=self.start_game)

    def run_menu_swith(self, **cfg):
        self.menubar.entryconfigure(2, cfg)

    def run_menu_restart(self):
        self.menubar.entryconfigure(2, label='run', command=self.solve)

    def notify(self, message, severity=None):
        self.notif_text.notify(message, severity)

    def start(self):
        self.root.mainloop()

    def start_game(self):
        self.set_puzzle_and_tips_text()

    def set_puzzle_and_tips_text(self):
        url = '{host}/asterios/{team}/member/{member_id}'.format(
            host=VARIABLES.host.get(),
            team=VARIABLES.team.get(),
            member_id=VARIABLES.member_id.get()
        )

        request = Request(url, method='GET')  # headers=dict(headers)
        try:
            response = urlopen(request)  # timeout=120
        except HTTPError as error:
            msg = error.read().decode('utf-8')

            try:
                data = json.loads(msg)
            except:
                self.notify(msg)
            else:
                if data['exception'] == 'LevelSet.DoneException':
                    GameOverToplevel(self.root, 'Victory')
                else:
                    self.notify(msg)

        else:
            data = json.loads(response.read().decode('utf-8'))
            self.tips_text.delete('0.0', 'end')
            self.tips_text.insert('end', data['tip'])
            self.puzzle_text.update_text(data['puzzle'])
            VARIABLES.puzzle = data['puzzle']

    def _display_solved_puzzle(self, solved_puzzle):
        """
        Shows a Toplevel containing the `solved_puzzle`.
        """
        top_level = Toplevel(self.root)
        viewer = PuzzleViewer(top_level)
        viewer.pack()
        viewer.update_text(solved_puzzle)
        top_level.wm_transient(self.root)

    def solve(self):

        def filter_traceback(tb):
            new_tb = tb[:1]
            iter_tb = iter(tb)
            expected_line = '  File "{}",'.format(VARIABLES.tmp_file)
            for line in iter_tb:
                if line.startswith(expected_line):
                    new_tb.append(line)
                    break
            new_tb.extend(iter_tb)
            return new_tb

        code = self.solver_text.get('0.0', 'end')
        with VARIABLES.tmp_file.open('w') as py_file:
            py_file.write(code)

        try:
            importlib.invalidate_caches()
            if hasattr(VARIABLES.module_solver, 'solve'):
                del VARIABLES.module_solver.solve

            if VARIABLES.module_solver is None:
                VARIABLES.module_solver = importlib.import_module(
                    'astrios_solver')
            else:
                VARIABLES.module_solver = importlib.reload(
                    VARIABLES.module_solver)

        except Exception as error:
            tb = traceback.format_exception(
                type(error), error, error.__traceback__)
            tb = filter_traceback(tb)
            self.notify(''.join(tb), 'error')
            return

        if not (hasattr(VARIABLES.module_solver, 'solve') and
                callable(VARIABLES.module_solver.solve)):
            self.notify('Error: `solve` function not found', 'error')
            return

        def target(queue):
            """
            Try to run solve function and push the result
            in the `queue` (True, result) or (False, error)
            """
            try:
                solution = VARIABLES.module_solver.solve(VARIABLES.puzzle)
                queue.put((True, solution))
            except Exception as error:
                tb = traceback.format_exception(
                    type(error), error, error.__traceback__)
                tb = filter_traceback(tb)
                queue.put((False, tb))

        queue = multiprocessing.Queue()
        process = multiprocessing.Process(
            target=target, args=(queue,))

        def wait_for_solver():
            try:
                success, solution_or_error = queue.get_nowait()
            except Empty:
                self.root.after(250, wait_for_solver)
                return None
            # except OSError:
            #

            if not success:
                self.notify(''.join(solution_or_error), 'error')
            else:
                self._display_solved_puzzle(solution_or_error)
                try:
                    solution = json.dumps(solution_or_error)
                except (ValueError, TypeError) as error:
                    self.notify('The solve function should return a'
                                ' JSON serializable object ({})'.format(error), 'error')
                    return

                url = '{host}/asterios/{team}/member/{member_id}'.format(
                    host=VARIABLES.host.get(),
                    team=VARIABLES.team.get(),
                    member_id=VARIABLES.member_id.get()
                )

                try:
                    # headers=dict(headers)
                    request = Request(url, method='POST')
                except ValueError:
                    self.notify('Error: Wrong url: `{}`'.format(url), 'error')
                else:
                    request.data = solution.encode('utf-8')
                    try:
                        response = urlopen(request)  # timeout=120
                    except HTTPError as error:
                        self.notify(json.loads(error.read().decode('utf-8')))

                    else:
                        self.notify(json.loads(
                            response.read().decode('utf-8')), 'success')
                        self.set_puzzle_and_tips_text()

            self.run_menu_restart()

        process.start()

        def kill():
            queue.put((False, 'killed'))
            process.terminate()
            self.run_menu_restart()

        self.run_menu_swith(label='kill', command=kill)
        self.root.after(0, wait_for_solver)
class Fenetre:
    def __init__(self, root, job):

        # Récupération de l'objet Jeu
        self.jeu = job
        self.saved = True

        # Création de la fenêtre principale
        self.root = root
        self.root.title('Rêve de Dragon')
        self.root.resizable(True, True)

        # Création des menus
        # On a 4 menu principaux : filemenu, cmdmenu, viewmenu et helpmenu
        self.menubar = Menu(root)
        self.root.config(menu=self.menubar)

        # filemenu: menu de manipulation des fichiers contenant les personnages
        self.filemenu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="Fichier", menu=self.filemenu)
        self.filemenu.add_command(label="Nouveau", command=self.nouveau)
        self.filemenu.add_command(label="Ouvrir", command=self.ouvrir)
        self.filemenu.add_separator()
        self.filemenu.add_command(label="Enregistrer",
                                  command=self.jeu.enregistrer)
        self.filemenu.add_command(label="Enregistrer sous...",
                                  command=self.jeu.enregistrer_sous)
        self.filemenu.add_separator()
        self.filemenu.add_command(label="Fermer", command=self.fermer)
        self.filemenu.add_separator()
        self.filemenu.add_command(label="Imprimer",
                                  command=self.void,
                                  state='disabled')
        self.filemenu.add_separator()
        self.filemenu.add_command(label="Quitter", command=self.quitter)

        # cmdmenu: menu des commandes sur les personnages
        self.cmdmenu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="Commande", menu=self.cmdmenu)
        self.cmdmenu.add_command(label="Nouvelle Partie", command=self.partie)
        self.cmdmenu.add_separator()
        self.cmdmenu.add_command(label="Nouveau Personnage",
                                 command=self.creer)
        self.cmdmenu.add_separator()
        self.cmdmenu.add_command(label="Valider le Personnage",
                                 command=self.valider)

        # viewmenu: menu de sélection du personnage à l'affichage
        # Ce menu est vide en l'absence de personnage
        # Il est rempli au chargement ou à la création d'un personnage
        self.viewmenu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="Personnage", menu=self.viewmenu)

        # helpmenu: menu d'aide
        self.helpmenu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="Aide", menu=self.helpmenu)
        self.helpmenu.add_command(label="Règles du Jeu", command=self.regles)
        self.helpmenu.add_command(label="Utilisation du Programme",
                                  command=self.utilise)
        self.helpmenu.add_command(label="A Propos...", command=self.a_propos)

        # frame1 : Fiche du personnage
        frame1 = Frame(root,
                       borderwidth=0,
                       relief='flat',
                       height=200,
                       width=600)
        frame1.grid(row=0, column=0, sticky='NW', padx="10", pady="5")

        # Nom
        self.Entry_Nom = StringVar()
        self.Old_Nom = ""
        Label(frame1, text='Nom:').grid(row=0,
                                        column=0,
                                        columnspan=2,
                                        sticky='E')
        Entry(frame1, textvariable=self.Entry_Nom, justify='left', width=34)\
            .grid(row=0, column=2, columnspan=4, sticky='W', padx="5")

        # Age
        self.Entry_Age = IntVar()
        Label(frame1, text='Age:').grid(row=1,
                                        column=0,
                                        columnspan=2,
                                        sticky='E')
        Entry(frame1, textvariable=self.Entry_Age, justify='right', width=3)\
            .grid(row=1, column=2, sticky='W', padx="5")

        # Heure de naissance (pour hauts-rêvants)
        self.Entry_Heure = IntVar()
        Label(frame1, text='Heure de Naissance:').grid(row=1,
                                                       column=3,
                                                       sticky='E')
        Entry(frame1, textvariable=self.Entry_Heure, justify='right', width=3) \
            .grid(row=1, column=4, sticky='W', padx="5")

        # Taille
        self.Entry_Taille = IntVar()
        Label(frame1, text='Taille:').grid(row=1, column=5, sticky='E')
        Entry(frame1, textvariable=self.Entry_Taille, justify='right', width=3)\
            .grid(row=1, column=6, sticky='W', padx="5")

        # Poids
        self.Entry_Poids = IntVar()
        Label(frame1, text='Poids:').grid(row=1, column=7, sticky='E')
        Entry(frame1, textvariable=self.Entry_Poids, justify='right', width=3)\
            .grid(row=1, column=8, sticky='W', padx="5")

        # Beauté
        self.Entry_Beaute = IntVar()
        Label(frame1, text='Beauté:').grid(row=2,
                                           column=0,
                                           columnspan=2,
                                           sticky='E')
        Entry(frame1, textvariable=self.Entry_Beaute, justify='right', width=3) \
            .grid(row=2, column=2, sticky='W', padx="5")

        # Cheveux
        self.Entry_Cheveux = StringVar()
        Label(frame1, text='Cheveux:').grid(row=2, column=3, sticky='E')
        Entry(frame1, textvariable=self.Entry_Cheveux, justify='left', width=8)\
            .grid(row=2, column=4, sticky='W', padx="5")

        # Yeux
        self.Entry_Yeux = StringVar()
        Label(frame1, text='Yeux:').grid(row=2, column=5, sticky='E')
        Entry(frame1, textvariable=self.Entry_Yeux, justify='left', width=8)\
            .grid(row=2, column=6, sticky='W', padx="5")

        # Haut rêvant
        self.Entry_HRevant = IntVar()
        Checkbutton(frame1, text="Haut-Rêvant", variable=self.Entry_HRevant, command=self.sel_revant) \
            .grid(row=2, column=7, columnspan=2, sticky='W', padx="5")

        # Sexe
        self.Entry_Sexe = StringVar()
        Label(frame1, text='Sexe:').grid(row=3,
                                         column=0,
                                         columnspan=2,
                                         sticky='E')
        Entry(frame1, textvariable=self.Entry_Sexe, justify='left', width=2)\
            .grid(row=3, column=2, sticky='W', padx="5")

        # Ambidextre
        self.Entry_Ambidextre = IntVar()
        Label(frame1, text='Ambidextre:').grid(row=3, column=3, sticky='E')
        Entry(frame1, textvariable=self.Entry_Ambidextre, justify='right', width=3)\
            .grid(row=3, column=4, sticky='W', padx="5")

        # Signes Particuliers
        self.Entry_SignesP = StringVar()
        Label(frame1, text='Signes Particuliers:').grid(row=3,
                                                        column=5,
                                                        sticky='E')
        Entry(frame1, textvariable=self.Entry_SignesP, justify='left', width=37)\
            .grid(row=3, column=6, columnspan=3, sticky='W', padx="5")

        # Frame 2 : Caractéristiques
        frame2 = LabelFrame(root,
                            text=" Caractéristiques ",
                            borderwidth=2,
                            relief='ridge',
                            height=200,
                            width=600)
        frame2.grid(row=1, column=0, sticky='NW', padx="10", pady="5")
        frame20 = LabelFrame(frame2,
                             text=' Physiques ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=200)
        frame20.grid(row=0, column=0, sticky='NW', padx="5", pady="5")
        frame21 = LabelFrame(frame2,
                             text=' Mentales ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=200)
        frame21.grid(row=0, column=1, sticky='NW', padx="5", pady="5")
        frame22 = LabelFrame(frame2,
                             text=' Pouvoirs ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=200)
        frame22.grid(row=0, column=2, sticky='NW', padx="5", pady="5")
        frame23 = LabelFrame(frame2,
                             text=' Dérivées ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=200)
        frame23.grid(row=0, column=3, sticky='NW', padx="5", pady="5")
        self.Entry_C = []

        # Colonne 0 de taille à Dextérité
        for i in range(0, 6):
            self.Entry_C.append(IntVar())
            Label(frame20, text="           "+personnage.caracteristique(i, 1)+':')\
                .grid(row=i, column=0, sticky='E')
            Entry(frame20, textvariable=self.Entry_C[i], justify='right', width=3)\
                .grid(row=i, column=1, sticky='W', padx="5")
        Label(frame20, text=' ').grid(row=6, column=0, sticky='E')

        # Colonne 1 de Vue à Empathie
        for i in range(6, 12):
            self.Entry_C.append(IntVar())
            Label(frame21, text="           "+personnage.caracteristique(i, 1) + ':')\
                .grid(row=i-6, column=0, sticky='E')
            Entry(frame21, textvariable=self.Entry_C[i], justify='right', width=3)\
                .grid(row=i-6, column=1, sticky='W', padx="5")
        Label(frame21, text=' ').grid(row=6, column=0, sticky='E')

        # Colonne 2 de Rêve à Chance
        for i in range(12, 14):
            self.Entry_C.append(IntVar())
            Label(frame22, text="               "+personnage.caracteristique(i, 1) + ':')\
                .grid(row=i-12, column=0, sticky='E')
            Entry(frame22, textvariable=self.Entry_C[i], justify='right', width=3)\
                .grid(row=i-12, column=1, sticky='W', padx="5")
        for i in range(2, 7):
            Label(frame22, text=' ').grid(row=i, column=0, sticky='E')

        # Colonne 3 de Tir à Dérobée (ne peuvent être saisies)
        for i in range(14, 18):
            self.Entry_C.append(IntVar())
            Label(frame23, text="             "+personnage.caracteristique(i, 1) + ':')\
                .grid(row=i-14, column=0, sticky='E')
            Entry(frame23, textvariable=self.Entry_C[i], justify='right', width=3, state='disabled')\
                .grid(row=i-14, column=1, sticky='W', padx="5")
        for i in range(4, 7):
            Label(frame23, text=' ').grid(row=i, column=0, sticky='E')

        # frame 3 : Points et Seuils (ne peuvent être saisis)
        frame3 = Frame(root,
                       borderwidth=0,
                       relief='flat',
                       height=200,
                       width=600,
                       padx="5",
                       pady="5")
        frame3.grid(row=2, column=0, sticky='NW', padx="10")
        self.Entry_P = []

        # Vie - Endurance - Encombrement
        for i in range(0, 3):
            self.Entry_P.append(IntVar())
            Label(frame3, text=personnage.point(i, 1) + ':').grid(row=0,
                                                                  column=2 * i,
                                                                  sticky='E')
            Entry(frame3, textvariable=self.Entry_P[i], justify='right', width=3, state='disabled')\
                .grid(row=0, column=2*i+1, sticky='W', padx="5")

        # Bonus aux Dommages - Malus Armure - Seuil de Constitution - Seuil de Sustentation
        for i in range(3, 7):
            self.Entry_P.append(IntVar())
            Label(frame3,
                  text=personnage.point(i, 1) + ':').grid(row=1,
                                                          column=2 * i - 6,
                                                          sticky='E')
            Entry(frame3, textvariable=self.Entry_P[i], justify='right', width=3, state='disabled')\
                .grid(row=1, column=2*i-5, sticky='W', padx="5")

        # frame 4 : Compétences
        frame4 = LabelFrame(root,
                            text=" Compétences ",
                            borderwidth=2,
                            relief='ridge',
                            height=200,
                            width=800)
        frame4.grid(row=3,
                    column=0,
                    columnspan=2,
                    sticky='NW',
                    padx="10",
                    pady="5")
        frame40 = LabelFrame(frame4,
                             text=' Générales ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=300)
        frame40.grid(row=0,
                     column=0,
                     rowspan=2,
                     sticky='NW',
                     padx="5",
                     pady="5")
        frame41 = LabelFrame(frame4,
                             text=' Particulières ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=300)
        frame41.grid(row=0,
                     column=1,
                     rowspan=2,
                     sticky='NW',
                     padx="5",
                     pady="5")
        frame42 = LabelFrame(frame4,
                             text=' Spécialisées ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=300)
        frame42.grid(row=0,
                     column=2,
                     rowspan=2,
                     sticky='NW',
                     padx="5",
                     pady="5")
        frame43 = LabelFrame(frame4,
                             text=' Connaissances ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=300)
        frame43.grid(row=0, column=3, sticky='NW', padx="5", pady="5")
        frame44 = LabelFrame(frame4,
                             text=' Draconic ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=300)
        frame44.grid(row=1, column=3, sticky='SW', padx="5", pady="5")
        frame45 = LabelFrame(frame4,
                             text=' Combat Mélée ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=300)
        frame45.grid(row=0,
                     column=4,
                     rowspan=2,
                     sticky='NW',
                     padx="5",
                     pady="5")
        frame46 = LabelFrame(frame4,
                             text=' Combat Tir-Lancer ',
                             borderwidth=2,
                             relief='ridge',
                             height=200,
                             width=300)
        frame46.grid(row=0,
                     column=5,
                     rowspan=2,
                     sticky='NW',
                     padx="5",
                     pady="5")
        self.Entry_A = []

        # Colonne 0 : Générales
        for i in range(0, 11):
            self.Entry_A.append(IntVar())
            Label(frame40,
                  text="           " + personnage.competence(i, 2) + ':').grid(
                      row=i, column=0, sticky='E')
            Entry(frame40, textvariable=self.Entry_A[i], justify='right', width=3)\
                .grid(row=i, column=1, sticky='W', padx="5")
        for i in range(11, 15):
            Label(frame40, text=' ').grid(row=i, column=0, sticky='E')

        # Colonne 1 : Particulières
        for i in range(11, 26):
            self.Entry_A.append(IntVar())
            Label(frame41, text="      " + personnage.competence(i, 2) +
                  ':').grid(row=i - 11, column=0, sticky='E')
            Entry(frame41, textvariable=self.Entry_A[i], justify='right', width=3)\
                .grid(row=i-11, column=1, sticky='W', padx="5")

        # Colonne 2 : Spécialisées
        for i in range(26, 36):
            self.Entry_A.append(IntVar())
            Label(frame42, text="       "+personnage.competence(i, 2)+':')\
                .grid(row=i-25, column=0, sticky='E')
            Entry(frame42, textvariable=self.Entry_A[i], justify='right', width=3)\
                .grid(row=i-25, column=1, sticky='W', padx="5")
        for i in range(10, 15):
            Label(frame42, text=' ').grid(row=i + 1, column=0, sticky='E')

        # Colonne 3: Connaissances
        for i in range(36, 43):
            self.Entry_A.append(IntVar())
            Label(frame43, text="         "+personnage.competence(i, 2)+':')\
                .grid(row=i-35, column=0, sticky='E')
            Entry(frame43, textvariable=self.Entry_A[i], justify='right', width=3)\
                .grid(row=i-35, column=1, sticky='W', padx="5")
        Label(frame43, text=' ').grid(row=8, column=0, sticky='E')

        # Colonne 3 : Draconic
        self.Draconic = []
        for i in range(0, 4):
            self.Entry_A.append(IntVar())
            Label(frame44, text="             "+personnage.competence(i+43, 2)+':')\
                .grid(row=i, column=0, sticky='E')
            self.Draconic.append(
                Entry(frame44,
                      textvariable=self.Entry_A[i + 43],
                      justify='right',
                      width=3))
            self.Draconic[i].grid(row=i, column=1, sticky='W', padx="5")
        Label(frame44, text=' ').grid(row=4, column=0, sticky='E')

        # Colonne 4 : Combat Mélée
        for i in range(47, 60):
            self.Entry_A.append(IntVar())
            Label(frame45, text=personnage.competence(i, 2) + ':') \
                .grid(row=i - 46, column=0, sticky='E')
            Entry(frame45, textvariable=self.Entry_A[i], justify='right', width=3) \
                .grid(row=i - 46, column=1, sticky='W', padx="5")
        for i in range(13, 15):
            Label(frame45, text=' ').grid(row=i + 1, column=0, sticky='E')

        # Colonne 5 : Combat Tir
        for i in range(60, 66):
            self.Entry_A.append(IntVar())
            Label(frame46, text="         " + personnage.competence(i, 2) + ':') \
                .grid(row=i - 59, column=0, sticky='E')
            Entry(frame46, textvariable=self.Entry_A[i], justify='right', width=3) \
                .grid(row=i - 59, column=1, sticky='W', padx="5")
        for i in range(6, 15):
            Label(frame46, text=' ').grid(row=i + 1, column=0, sticky='E')

        # frame5 : table de résolution et lancer de dé
        frame5 = LabelFrame(root,
                            text=" Résolution et Lancer de Dés ",
                            borderwidth=2,
                            relief='ridge',
                            height=200,
                            width=600)
        frame5.grid(row=0,
                    column=1,
                    rowspan=3,
                    columnspan=2,
                    sticky='NW',
                    padx="10",
                    pady="5")

        # Listbox caractéristiques
        Label(frame5, text='         Caractéristique:').grid(row=0,
                                                             column=0,
                                                             columnspan=2,
                                                             padx="10",
                                                             sticky='NW')
        self.liste1 = Listbox(frame5, height=13, width=18, relief='sunken')
        self.liste1.grid(row=1, column=1, sticky='NW', pady="5")
        for i in range(0, 18):
            self.liste1.insert(i, personnage.caracteristique(i, 1))
        self.liste1.bind('<<ListboxSelect>>', self.sel_liste1)

        # Listbox compétences
        Label(frame5, text='Compétence:').grid(row=0,
                                               column=2,
                                               columnspan=2,
                                               padx="10",
                                               sticky='NW')
        self.liste2 = Listbox(frame5, height=13, width=18, relief='sunken')
        self.liste2.grid(row=1, column=3, sticky='NW', pady="5")
        for i in range(0, 66):
            self.liste2.insert(i, personnage.competence(i, 2))
        self.liste2.bind('<<ListboxSelect>>', self.sel_liste2)

        # Zone de résulats
        self.Entry_R_C_Val = IntVar()
        Entry(frame5, textvariable=self.Entry_R_C_Val, justify='right', width=3,) \
            .grid(row=16, column=0, sticky='E', padx="10")
        self.Entry_R_C_Name = StringVar()
        Entry(frame5, textvariable=self.Entry_R_C_Name, justify='left', width=18, state='disabled') \
            .grid(row=16, column=1, sticky='W')
        self.Entry_R_A_Val = IntVar()
        Entry(frame5, textvariable=self.Entry_R_A_Val, justify='right', width=3,) \
            .grid(row=16, column=2, sticky='E', padx="10")
        self.Entry_R_A_Name = StringVar()
        Entry(frame5, textvariable=self.Entry_R_A_Name, justify='left', width=18, state='disabled') \
            .grid(row=16, column=3, sticky='W')
        Label(frame5, text='   Seuil de Réussite:').grid(row=17,
                                                         column=0,
                                                         sticky='NE')
        self.Entry_R_Seuil = IntVar()
        Entry(frame5, textvariable=self.Entry_R_Seuil, justify='right', width=3, state='disabled')\
            .grid(row=17, column=1, sticky='W', padx="10")
        Label(frame5, text='Tirage:').grid(row=17, column=2, sticky='NE')
        self.Entry_R_Tirage = IntVar()
        Entry(frame5, textvariable=self.Entry_R_Tirage, justify='right', width=3, state='disabled') \
            .grid(row=17, column=3, sticky='W', padx="10")
        Label(frame5, text='Résultat Spécial:').grid(row=18,
                                                     column=0,
                                                     sticky='NE')
        self.Entry_R_Special = StringVar()
        Entry(frame5, textvariable=self.Entry_R_Special, justify='left', width=30, state='disabled') \
            .grid(row=18, column=1, columnspan=2, sticky='W', padx="10")
        Label(frame5, text=' ').grid(row=19, column=4, sticky='NE')

        # Bouton pour le lancer de Dés
        Button(frame5, text="Lancer les Dés", command=self.lancer) \
            .grid(row=18, column=3, columnspan=3, sticky='W', padx="10")

        # La mascote
        # On la fait déborder sur le frame4 pour gagner en largeur totale
        self.dragon = PhotoImage(file='./dragon3.gif')
        logo = Canvas(root, width=200, height=181, bd=1, relief='ridge')
        logo.grid(row=3,
                  column=1,
                  columnspan=2,
                  sticky='SE',
                  padx="10",
                  pady="3")
        logo.create_image(0, 0, image=self.dragon, anchor='nw')

        # L'ecran étant initialisé, on peut créér un premier personnage par défaut
        self.creer()
        return

    # Fonction de recopie de la sélection depuis la Listbox des caractéristiques
    # Met à jour les 2 champs points et nom de caractéristique pour le calcul de résolution
    def sel_liste1(self, event):

        if self.liste1.curselection() != ():
            index = self.liste1.curselection()[0]
            self.Entry_R_C_Name.set(self.liste1.get(index))
            self.Entry_R_C_Val.set(self.Entry_C[index].get())
        return

    # Fonction de recopie de la sélection depuis la Listbox des compétences
    # Met à jour les 2 champs points et nom de compétence pour le calcul de résolution
    def sel_liste2(self, event):

        if self.liste2.curselection() != ():
            index = self.liste2.curselection()[0]
            self.Entry_R_A_Name.set(self.liste2.get(index))
            self.Entry_R_A_Val.set(self.Entry_A[index].get())
        return

    # Fonction de changement d'etat haut-rêvant
    def sel_revant(self):

        if self.Entry_HRevant.get() != 1:
            for i in range(0, 4):
                self.Entry_A[i + 42].set(-11)
                self.Draconic[i].configure(state='disabled')
        else:
            for i in range(0, 4):
                self.Draconic[i].configure(state='normal')
        return

    # Nouveau jeu
    # Il faut préalablement fermer le jeu en cours
    def nouveau(self):

        if self.fermer():
            self.jeu.nouveau()
        return

    # Ouvrir jeu
    # Il faut préalablement fermer le jeu en cours
    # On reçoit le nom du jeu suivi d'une liste de personnages ou None si rien d'ouvert par le jeu
    def ouvrir(self):

        if self.fermer():
            names = self.jeu.ouvrir()
            if names != None:
                numero = -1
                for person in names:

                    # index 0 : nom du fichier jeu
                    if numero < 0:
                        self.root.title('Rêve de Dragon - ' + person)

                    # autres index : personnages
                    # index vaudra le nombre de personnages reçus
                    else:
                        self.viewmenu.add_command(label=person,
                                                  command=lambda index=numero:
                                                  self.selectionner(index))
                    numero += 1

                # On affiche le premier personnage
                if numero > 0:
                    self.selectionner(0)

        return

    # Fermer le jeu en cours
    # On efface tous les personnages
    # on cree un nouveau personnage vide pour obtenir un affichage vierge
    def fermer(self):

        if not self.saved:
            self.saved = askyesno(
                'Fermer',
                'Voulez-vous vraiment fermer ce Jeu ?\nLes données non enregistrées seront perdues'
            )
        if self.saved:
            last = self.viewmenu.index("end")
            if last is not None:
                for i in range(last + 1):
                    self.viewmenu.delete(0)
            self.root.title('Rêve de Dragon')
            self.jeu.fermer()
            self.creer()
        return self.saved

    # Quitter le programme
    # onh détruit la fenêtre et on quitte
    def quitter(self):

        if askyesno('Quitter', 'Voulez-vous vraiment quitter le programme ?'):
            self.root.destroy()
            self.root.quit()
        return

    # Fonction interne d'affichage des données d'un personnage
    # Copie toutes les données du dictionnaire local dans les variables associées aux champs de saisie
    def affiche(self):

        self.Entry_Nom.set(self.pod["Fiche"]["Nom"])
        self.Entry_Age.set(self.pod["Fiche"]["Age"])
        self.Entry_Heure.set(self.pod["Fiche"]["Heure_Naissance"])
        self.Entry_Taille.set(self.pod["Fiche"]["Taille"])
        self.Entry_Poids.set(self.pod["Fiche"]["Poids"])
        self.Entry_Sexe.set(self.pod["Fiche"]["Sexe"])
        self.Entry_Cheveux.set(self.pod["Fiche"]["Cheveux"])
        self.Entry_Yeux.set(self.pod["Fiche"]["Yeux"])
        self.Entry_Beaute.set(self.pod["Fiche"]["Beaute"])
        self.Entry_Ambidextre.set(self.pod["Fiche"]["Ambidextre"])
        self.Entry_HRevant.set(self.pod["Fiche"]["Haut_Revant"])
        self.Entry_SignesP.set(self.pod["Fiche"]["Signes_Particulier"])
        for i in range(0, 18):
            self.Entry_C[i].set(
                self.pod["Caracteristique"][personnage.caracteristique(i, 0)])
        for i in range(0, 7):
            self.Entry_P[i].set(self.pod["Point"][personnage.point(i, 0)])
        for i in range(0, 66):
            self.Entry_A[i].set(self.pod["Competence"][personnage.competence(
                i, 0)][personnage.competence(i, 1)])
        if self.Entry_HRevant.get() != 1:
            for i in range(0, 4):
                self.Draconic[i].configure(state='disabled')
        return

    # Création d'un nouveau personnage
    # On demande au jeu de créer un nouveau personnage dans la liste
    # On initialise toutes les variables de saisie aux valeur reçues
    def creer(self):

        self.pod = self.jeu.creer()
        self.affiche()
        return

    # Validation des données du personnage
    # On reconstitue le dictionnaire qui est envoyé au jeu pour vérification
    # Le jeu répond avec un dictionnaire contenant
    # - l'index du personnage
    # - Le nom de personnage
    # - Un message d'erreur ou d'acceptation
    def valider(self):

        if len(self.Entry_Nom.get()) < 1:
            return

        self.pod["Fiche"]["Nom"] = self.Entry_Nom.get()
        self.pod["Fiche"]["Age"] = self.Entry_Age.get()
        self.pod["Fiche"]["Heure_Naissance"] = self.Entry_Heure.get()
        self.pod["Fiche"]["Taille"] = self.Entry_Taille.get()
        self.pod["Fiche"]["Poids"] = self.Entry_Poids.get()
        self.pod["Fiche"]["Sexe"] = self.Entry_Sexe.get()
        self.pod["Fiche"]["Cheveux"] = self.Entry_Cheveux.get()
        self.pod["Fiche"]["Yeux"] = self.Entry_Yeux.get()
        self.pod["Fiche"]["Beaute"] = self.Entry_Beaute.get()
        self.pod["Fiche"]["Ambidextre"] = self.Entry_Ambidextre.get()
        self.pod["Fiche"]["Haut_Revant"] = self.Entry_HRevant.get()
        self.pod["Fiche"]["Signes_Particulier"] = self.Entry_SignesP.get()
        for i in range(0, 18):
            self.pod["Caracteristique"][personnage.caracteristique(
                i, 0)] = self.Entry_C[i].get()
        for i in range(0, 7):
            self.pod["Point"][personnage.point(i, 0)] = self.Entry_P[i].get()
        for i in range(0, 65):
            self.pod["Competence"][personnage.competence(
                i, 0)][personnage.competence(i, 1)] = self.Entry_A[i].get()
        retour = self.jeu.valider(self.pod)
        index = retour["index"]

        # On a bien un index valide : alors on met à jour le menu et on va chercher les données du personnage
        if index is not None:
            if self.viewmenu.entrycget(index, 'label') != self.Old_Nom:
                self.viewmenu.entryconfigure(index, label=retour["nom"])
            elif self.viewmenu.entrycget(index, 'label') != retour["nom"]:
                self.viewmenu.add_command(
                    label=retour["nom"],
                    command=lambda index=index: self.selectionner(index))
            self.pod = self.jeu.selectionner(index)
            self.affiche()

        # En cas d'erreur ou retour ok, il y a un message du jeu
        if len(retour["message"]):
            showerror("Validation", retour["message"])

        self.saved = False
        return

    # Sélection d'un personnage depuis le menu
    # On envoie au jeu l'index du menu qui correspond à l'index de la liste du jeu
    # Les données du personnage reçu sont ensuite affichées
    def selectionner(self, code):

        self.pod = self.jeu.selectionner(code)
        self.affiche()
        return

    # Nouvelle partie
    # On demande au Jeu de changer de partie
    # Le jeu renvoie le contenu du personnage courant
    def partie(self):

        if askyesno(
                'Nouvelle Partie',
                'Voulez-vous vraiment terminer cette partie ?\nLes données non enregistrées seront perdues'
        ):
            self.pod = self.jeu.partie()
            self.affiche()
        return

    # Lancer de dé
    # Calcul de résolution puis lancer des dés
    # On passe au jeu les valeurs de caractéristiques et compétences sélectionnées
    # Le résultat sera récupéré en retour et affiché
    def lancer(self):

        resultat = self.jeu.lancer(self.Entry_R_C_Val.get(),
                                   self.Entry_R_A_Val.get())
        self.Entry_R_Seuil.set(resultat["seuil"])
        self.Entry_R_Tirage.set(resultat["tirage"])
        self.Entry_R_Special.set(resultat["special"])
        self.saved = False
        return

    # Affichage de la boite de dialogue A propos
    # Le texte est dans le fichier A_PROPOS.TXT
    def a_propos(self):

        fp = open("A_PROPOS.TXT", "r")
        texte_a_propos = fp.read()
        fp.close()
        showinfo("A Propos de...", texte_a_propos)
        return

    # Dialogue d'aide pour connaitre les règles du jeu
    # Le texte est dans le fichier REGLES.TXT
    def regles(self):

        file = "REGLE.TXT"
        titre = "Règles du Jeu Rêve de Dragon."
        self.aide(file, titre)
        return

    # Le texte est dans le fichier AIDE.TXT
    def utilise(self):

        file = "AIDE.TXT"
        titre = "Utilisation de Rêve de Dragon."
        self.aide(file, titre)
        return

    # Aide du jeu
    # Affiche une boite de dialogue avec un widget texte contenant l'aide
    def aide(self, file, titre):

        # On ouvre une fenêtre fille de celle du jeu
        self.wdw = Toplevel()
        self.wdw.geometry('+400+100')
        self.wdw.title(titre)

        # Le texte de l'aide est stocké dans un fichier Atexte
        fp = open(file, "r")
        texte_aide = fp.read()
        fp.close()

        # On l'affiche dans un widget Text avec une barre de défilement
        # Ne fonctionne que si on utilise grid pour placer les Widgets
        # Il faut mettre le widget en état disabled pour éviter que l'on y entre du texte
        self.S = Scrollbar(self.wdw, orient='vertical')
        self.T = Text(self.wdw,
                      height=50,
                      width=100,
                      font=('TkDefaultFont', 10))
        self.T.grid(row=0, column=0, sticky='NW')
        self.S.configure(command=self.T.yview)
        self.T.configure(yscrollcommand=self.S.set)
        self.S.grid(row=0, column=1, sticky='SN')
        self.T.configure(state='normal')
        self.T.insert('end', texte_aide)
        self.T.configure(state='disabled')

        # La nouvelle fenêtre est ouverte en modal
        # Il faudra la fermer pour reprendre le contrôle de la fenêtre principale
        self.wdw.transient(self.root)
        self.wdw.grab_set()
        self.root.wait_window(self.wdw)
        return

    # fonction qui ne fait rien (pour les tests)
    def void(self):
        return
text_menu.add_command(label="粘贴", command=callback)
text_menu.add_command(label="撤销", command=callback)
text_menu.add_separator()
text_menu.add_command(label="重置", command=callback)
text_menu.add_command(label="新建窗口", command=createWindow)

# 一级菜单,直接放在菜单栏上,窗口菜单栏上不会生效
menuBar.add_separator()
menuBar.add_command(label="重置2", command=callback)  # 窗口菜单栏不现实
menuBar.add_command(label="新建窗口2", command=createWindow)  # 窗口菜单栏不现实

# 没有置顶menu参数的情况下,在顶部菜单栏是无效的
menuBar.add_cascade(label="顶部菜单栏是无效的")

# 禁用某个菜单
menuBar.entryconfigure(2, state='disabled')

frame = Frame(window, width=512, height=512)
frame.pack()


def popup(event):
    # print(event.x_root + 10, event.y_root + 10)
    menuBar.post(event.x_root + 1, event.y_root)


# frame绑定事件popup
frame.bind("<Button-2>", popup)
# or
# frame.bind("<ButtonPress-2>", popup)
Exemple #11
0
class CostumEntry(Entry):
    def __init__(self, *args, **kwargs):
        Entry.__init__(self, *args, **kwargs)
        self.config(highlightthickness=1, relief='flat')
        self.config(highlightbackground="#AAA", highlightcolor="#AAA")

        self.changes = [""]
        self.steps = int()

        self.context_menu = Menu(self, tearoff=0)
        self.context_menu.add_command(label="Cut")
        self.context_menu.add_command(label="Copy")
        self.context_menu.add_command(label="Paste")

        self.bind("<KeyRelease>", self.on_change)

        self.bind("<Control-z>", self.undo)
        self.bind("<Control-Shift-Z>", self.redo)

        self.bind("<Button-3>", self.popup)

        # self.focus_set()

    def flash(self):

        self.config(highlightbackground="red", highlightcolor="red")
        self.update_idletasks()
        time.sleep(0.06)
        self.config(highlightbackground="white", highlightcolor="white")
        self.update_idletasks()

        time.sleep(0.05)
        self.config(highlightbackground="red", highlightcolor="red")
        self.update_idletasks()
        time.sleep(0.04)
        self.config(highlightbackground="white", highlightcolor="white")
        self.update_idletasks()

        time.sleep(0.03)
        self.config(highlightbackground="red", highlightcolor="red")
        self.update_idletasks()
        time.sleep(0.02)
        self.config(highlightbackground="white", highlightcolor="white")
        self.update_idletasks()

        self.config(highlightbackground="#DDD", highlightcolor="#DDD")

    def on_change(self, event=None):
        if self.get() != self.changes[-1]:
            self.changes.append(self.get())
            self.steps += 1

    def popup(self, event):
        self.context_menu.post(event.x_root, event.y_root)
        self.context_menu.entryconfigure(
            "Cut", command=lambda: self.event_generate("<<Cut>>"))
        self.context_menu.entryconfigure(
            "Copy", command=lambda: self.event_generate("<<Copy>>"))
        self.context_menu.entryconfigure(
            "Paste", command=lambda: self.event_generate("<<Paste>>"))

    def undo(self, event=None):
        if self.steps != 0:
            self.steps -= 1
            self.delete(0, 'end')
            self.insert('end', self.changes[self.steps])

    def redo(self, event=None):
        if self.steps < len(self.changes):
            self.delete(0, 'end')
            self.insert('end', self.changes[self.steps])
            self.steps += 1
class GUIDatasetClasificacion():
    def __init__(self, root):
        self.root = root

        #TODO Esta información se lee desde .cfg con ConfigParser
        self.ruta_datasets = ''
        self.ruta_resultados = ''
        self.rutas_relativas = BooleanVar(root, True)
        self.usa_sha1 = BooleanVar(root, True)
        self._tamanyo_muestra = 5
        self.clase_al_final = BooleanVar(root, True)
        self.mostrar_proceso = BooleanVar(root, False)

        self.lee_configuracion()

        estilo_bien = Style()
        estilo_bien.configure('G.TLabel', foreground='green')
        estilo_mal = Style()
        estilo_mal.configure('R.TLabel', foreground='red')

        self.crea_GUI()

        self.root.title(APP_NAME)
        #        self.root.update()
        #        self.root.minsize(self.root.winfo_width(), self.root.winfo_height())
        self.root.minsize(800, 500)

        #        self.lee_configuracion()

        self.root.protocol('WM_DELETE_WINDOW', self.cerrar_aplicacion)

    def crea_GUI(self):
        root = self.root
        #Menús
        self.menubar = Menu(root, tearoff=0)

        m_archivo = Menu(self.menubar, tearoff=0)
        m_archivo.add_command(label='Abrir',
                              command=self.abrir_dataset,
                              accelerator='Ctrl+O')
        m_archivo.add_separator()
        m_archivo.add_command(label='Salir', command=self.cerrar_aplicacion)
        self.menubar.add_cascade(label='Archivo', menu=m_archivo)

        m_proyecto = Menu(self.menubar, tearoff=0)
        m_proyecto.add_command(label='Abrir',
                               command=self.abrir_proyecto,
                               state="disabled")
        m_proyecto.add_separator()
        m_proyecto.add_checkbutton(label='Clase al final',
                                   onvalue=True,
                                   offvalue=False,
                                   variable=self.clase_al_final)
        self.menubar.add_cascade(label='Proyecto', menu=m_proyecto)

        self.m_configuracion = Menu(self.menubar, tearoff=0)
        self.m_configuracion.add_command(
            label='Ruta datasets', command=lambda: self.rutas('datasets'))
        self.m_configuracion.add_command(
            label='Ruta resultados', command=lambda: self.rutas('resultados'))
        self.m_configuracion.add_checkbutton(label='Rutas relativas',
                                             onvalue=True,
                                             offvalue=False,
                                             variable=self.rutas_relativas,
                                             command=self.cambia_rutas)
        self.m_configuracion.add_separator()
        #TODO Revisar self.v_tamanyo_muestra, no la uso
        #        self.v_tamanyo_muestra = StringVar(root, 'Tamaño muestra ({:,})'.\
        #                                           format(self._tamanyo_muestra))
        self.m_cfg_tamanyo_muestra = \
            self.m_configuracion.add_command(label='Tamaño muestra ({:,})'.\
                                           format(self._tamanyo_muestra),
                                        command=lambda: self.tamanyo_muestra(\
                                                        self._tamanyo_muestra))
        self.m_configuracion.add_separator()
        self.m_configuracion.add_checkbutton(label='Utiliza sha1',
                                             onvalue=True,
                                             offvalue=False,
                                             variable=self.usa_sha1)
        self.menubar.add_cascade(label='Configuración',
                                 menu=self.m_configuracion)

        m_ver = Menu(self.menubar, tearoff=0)
        self.v_tipo_dataset = StringVar(self.root, 'Dataset original')
        m_ver.add_radiobutton(label='Dataset original',
                              value='Dataset original',
                              variable=self.v_tipo_dataset,
                              command=self.muestra_atributos_y_clase)
        m_ver.add_radiobutton(label='Dataset sin evidencias incompletas',
                              value='Dataset sin evidencias incompletas',
                              variable=self.v_tipo_dataset,
                              command=self.muestra_atributos_y_clase)
        m_ver.add_radiobutton(label='Dataset sin atributos constantes',
                              value='Dataset sin atributos constantes',
                              variable=self.v_tipo_dataset,
                              command=self.muestra_atributos_y_clase)
        m_ver.add_radiobutton(label='Catálogo',
                              value='Catálogo',
                              variable=self.v_tipo_dataset,
                              command=self.muestra_atributos_y_clase)
        m_ver.add_radiobutton(label='Catálogo Robusto',
                              value='Catálogo Robusto',
                              variable=self.v_tipo_dataset,
                              command=self.muestra_atributos_y_clase)
        m_ver.add_separator()
        m_ver.add_checkbutton(label='Log del proceso',
                              onvalue=True,
                              offvalue=False,
                              variable=self.mostrar_proceso,
                              state='disabled')
        self.menubar.add_cascade(label='Ver', menu=m_ver)

        root.config(menu=self.menubar)

        #Dataset de clasificación
        lf_dataset = LabelFrame(root, text='Dataset de Clasificación')
        lf_dataset.pack(fill='both', expand=True, padx=5, pady=5)

        Label(lf_dataset, text='Nombre:').grid(row=0, column=0, sticky='e')
        self.v_nombre_dataset = StringVar(root, '-------')
        self.l_nombre_dataset = Label(lf_dataset,
                                      textvariable=self.v_nombre_dataset)
        self.l_nombre_dataset.grid(row=0, column=1, sticky='w')

        Label(lf_dataset, text='Tamaño:').grid(row=0, column=2, sticky='e')
        self.v_tamanyo_dataset = StringVar(root, '-------')
        Label(lf_dataset, textvariable=self.v_tamanyo_dataset).grid(row=0,
                                                                    column=3,
                                                                    sticky='w')

        Label(lf_dataset, text='Ubicación:').grid(row=1, column=0, sticky='e')
        self.v_ruta_dataset = StringVar(root, '-------------------------')
        #TODO Expandir en columnas 1-3, puede ser muy larga
        Label(lf_dataset, textvariable=self.v_ruta_dataset).grid(row=1,
                                                                 column=1,
                                                                 sticky='w',
                                                                 columnspan=3)

        #Dataset de clasificación / Muestra
        lf_dataset_muestra = LabelFrame(lf_dataset, text='Muestra')
        lf_dataset_muestra.grid(row=2,
                                column=0,
                                sticky='nsew',
                                columnspan=4,
                                padx=5,
                                pady=5)

        self.sb_v_t_muestra = Scrollbar(lf_dataset_muestra)
        self.sb_v_t_muestra.grid(row=0, column=1, sticky='sn')

        self.sb_h_t_muestra = Scrollbar(lf_dataset_muestra,
                                        orient='horizontal')
        self.sb_h_t_muestra.grid(row=1, column=0, sticky='ew')

        self.t_muestra = Text(lf_dataset_muestra,
                              yscrollcommand=self.sb_v_t_muestra.set,
                              xscrollcommand=self.sb_h_t_muestra.set,
                              bd=0,
                              wrap='none',
                              state='disabled',
                              height=8)
        self.t_muestra.grid(row=0, column=0, sticky='nswe')

        self.sb_v_t_muestra.config(command=self.t_muestra.yview)
        self.sb_h_t_muestra.config(command=self.t_muestra.xview)

        lf_dataset_muestra.rowconfigure(0, weight=1)
        lf_dataset_muestra.columnconfigure(0, weight=1)

        lf_dataset.rowconfigure(2, weight=3)
        lf_dataset.columnconfigure(1, weight=1)
        lf_dataset.columnconfigure(3, weight=1)

        #Dataset de clasificación / Evidencias
        lf_dataset_evidencias = LabelFrame(lf_dataset, text='Evidencias')
        lf_dataset_evidencias.grid(row=3,
                                   column=0,
                                   sticky='nsew',
                                   padx=5,
                                   pady=5)

        Label(lf_dataset_evidencias, text='Total:').grid(row=0,
                                                         column=0,
                                                         sticky='e')
        self.v_evidencias_total = StringVar(root, '-------')
        Label(lf_dataset_evidencias, textvariable=self.v_evidencias_total).\
              grid(row=0, column=1, sticky='w')
        Label(lf_dataset_evidencias, text='Completas:').grid(row=1,
                                                             column=0,
                                                             sticky='e')
        self.v_evidencias_completas = StringVar(root, '-------')
        Label(lf_dataset_evidencias, textvariable=self.v_evidencias_completas).\
              grid(row=1, column=1, sticky='w')
        Label(lf_dataset_evidencias, text='Únicas:').grid(row=2,
                                                          column=0,
                                                          sticky='e')
        self.v_evidencias_catalogo = StringVar(root, '-------')
        Label(lf_dataset_evidencias, textvariable=self.v_evidencias_catalogo).\
              grid(row=2, column=1, sticky='w')
        Label(lf_dataset_evidencias, text='Robustas:').grid(row=3,
                                                            column=0,
                                                            sticky='e')
        self.v_evidencias_robustas = StringVar(root, '-------')
        Label(lf_dataset_evidencias, textvariable=self.v_evidencias_robustas).\
              grid(row=3, column=1, sticky='w')

        #Dataset de clasificación / Atributos
        lf_dataset_clase_y_atributos = LabelFrame(lf_dataset,
                                                  text='Clase y atributos')
        lf_dataset_clase_y_atributos.grid(row=3,
                                          column=1,
                                          sticky='nsew',
                                          columnspan=3,
                                          padx=5,
                                          pady=5)

        PROPIEDADES_ATRIBUTOS = ('Nombre', 'count', 'unique', 'top', 'freq',
                                 'mean', 'std', 'min', '25%', '50%', '75%',
                                 'max')

        self.sb_h_tv_clase = Scrollbar(lf_dataset_clase_y_atributos,
                                       orient='horizontal')
        self.sb_h_tv_clase.grid(row=1, column=0, sticky='ew')
        self.tv_clase = Treeview(lf_dataset_clase_y_atributos,
                                 columns=PROPIEDADES_ATRIBUTOS,
                                 height=1,
                                 xscrollcommand=self.sb_h_tv_clase.set)
        self.tv_clase.grid(row=0, column=0, sticky='ew')
        self.sb_h_tv_clase.config(command=self.tv_clase.xview)
        self.tv_clase.heading("#0", text="#")
        self.tv_clase.column("#0", minwidth=30, width=40, stretch=False)

        self.sb_v_tv_atributos = Scrollbar(lf_dataset_clase_y_atributos)
        self.sb_v_tv_atributos.grid(row=2, column=1, sticky='sn')
        self.sb_h_tv_atributos = Scrollbar(lf_dataset_clase_y_atributos,
                                           orient='horizontal')
        self.sb_h_tv_atributos.grid(row=3, column=0, sticky='ew')
        self.tv_atributos = Treeview(lf_dataset_clase_y_atributos,
                                     columns=PROPIEDADES_ATRIBUTOS,
                                     yscrollcommand=self.sb_v_tv_atributos.set,
                                     xscrollcommand=self.sb_h_tv_atributos.set)
        self.tv_atributos.grid(row=2, column=0, sticky='nsew')
        self.tv_atributos.bind('<ButtonRelease-1>', self.selectItem)
        self.sb_v_tv_atributos.config(command=self.tv_atributos.yview)
        self.sb_h_tv_atributos.config(command=self.tv_atributos.xview)
        self.tv_atributos.heading("#0", text="#")
        self.tv_atributos.column("#0", minwidth=30, width=40, stretch=False)

        for i in PROPIEDADES_ATRIBUTOS:
            self.tv_clase.heading(i, text=i)
            self.tv_clase.column(i, minwidth=50, width=50, stretch=False)
            self.tv_atributos.heading(i, text=i)
            self.tv_atributos.column(i, minwidth=50, width=50, stretch=False)

        lf_dataset_clase_y_atributos.rowconfigure(2, weight=1)
        lf_dataset_clase_y_atributos.columnconfigure(0, weight=1)

        lf_dataset.rowconfigure(3, weight=1)

    def abrir_dataset(self):
        inicio = time.time()

        #TODO Las constantes se podrán modificar a través de .cfg
        nombre = askopenfilename(
            initialdir=self.ruta_datasets,
            filetypes=(('Archivos de valores separado por comas', '*.csv'),
                       ('Todos los archivos', '*.*')),
            title='Selecciona un Dataset de Clasificación')

        self.root.focus_force()

        if not nombre:
            return

        self.v_nombre_dataset.set(
            os.path.splitext(os.path.basename(nombre))[0])
        #TODO Esto debería hacerlo en otro sitio, no cuando lo elijo con
        #     filedialog, y tener en cuenta self.usa_sha1
        if os.path.exists(os.path.dirname(nombre)):
            self.l_nombre_dataset.configure(style='G.TLabel')
        else:
            self.l_nombre_dataset.configure(style='R.TLabel')
        self.v_tamanyo_dataset.set(tamanyo_legible(os.path.getsize(nombre)))

        self.v_ruta_dataset.set(os.path.relpath(os.path.dirname(nombre)) \
                                if self.rutas_relativas.get() else \
                                os.path.dirname(nombre))
        self.limpia_muestra()
        self.limpia_atributos_y_clase()
        self.root.update()

        #TODO Usar hilos o procesos para leer grandes datasets sin problemas
        #        self.progreso = Toplevel(self.root)
        #        self.progreso.title("Leyendo Dataset de Clasificación")
        #        barra = Progressbar(self.progreso, length=200, mode="indeterminate")
        #        barra.pack()
        #        self.q = Queue()
        #        hilo_lectura = Process(target=self.get_dc)
        #        hilo_lectura.start()
        ##        self.dc = self.q.get()
        ##        hilo_lectura.join()
        #        self.root.after(50, self.check_if_running, hilo_lectura, self.progreso)
        self.dc = DC(self.v_ruta_dataset.get(),
                     self.v_nombre_dataset.get(),
                     self.ruta_resultados,
                     guardar_resultados=False,
                     clase_al_final=self.clase_al_final.get(),
                     mostrar_proceso=self.mostrar_proceso,
                     num_filas_a_leer=None,
                     obtener_catalogo_robusto=False,
                     guardar_datos_proyecto=False,
                     mostrar_uso_ram=False)
        self.escribe_datos()

    def escribe_datos(self):
        self.escribe_muestra()
        self.v_evidencias_total.set('{:,}'.\
            format(self.dc.info_dataset_original.num_evidencias()))
        self.v_evidencias_completas.set('{:,}'.\
          format(self.dc.info_dataset_sin_datos_desconocidos.num_evidencias()))
        self.v_evidencias_catalogo.set('{:,}'.\
            format(self.dc.info_catalogo.num_evidencias()))
        self.v_evidencias_robustas.set('{:,}'.\
            format(self.dc.info_catalogo_robusto.num_evidencias()))

        self.muestra_atributos_y_clase()

        self.root.title('{} - {}'.format(APP_NAME,
                                         self.v_nombre_dataset.get()))

    def get_dc(self):
        #TODO No puedo crear self.dc en un try mientras depure DC
        #        try:
        self.dc = DC(self.v_ruta_dataset.get(),
                     self.v_nombre_dataset.get(),
                     self.ruta_resultados,
                     guardar_resultados=False,
                     clase_al_final=self.clase_al_final,
                     mostrar_proceso=self.mostrar_proceso,
                     num_filas_a_leer=None,
                     obtener_catalogo_robusto=False,
                     guardar_datos_proyecto=False,
                     mostrar_uso_ram=False)
        self.q.put(self.dc)


#        except Exception as e:
#            self.t_muestra.delete(1.0, 'end')
#            self.t_muestra.insert('end', e)

    def check_if_running(self, hilo, ventana):
        """Check every second if the function is finished."""
        if hilo.is_alive():
            self.root.after(50, self.check_if_running, hilo, ventana)
        else:
            ventana.destroy()
            self.escribe_datos()

    def abrir_proyecto(self):
        inicio = time.time()

        #TODO Diseñar estrategia para que el usuario sepa si ha de cambiarlo.
        #     Podría bastar con mostrarle los atributos y sus características.
        #        clase_al_final = self._clase_al_final

        #TODO Las constantes se podrán modificar a través de .cfg
        nombre = askopenfilename(initialdir=self.ruta_resultados,
                                 filetypes=(('Proyectos ACDC', '*.prjACDC'),
                                            ('Todos los archivos', '*.*')),
                                 title='Selecciona un proyecto ACDC')
        if nombre is None:
            return

        #TODO ¿Mostrar sólo el nombre del dataset original?
        self.root.title('{} - {}'.format(
            APP_NAME,
            os.path.splitext(os.path.basename(nombre))[0]))

    def rutas(self, r=None):
        ruta = askdirectory(title='Directorio de {}'.format(r),
                            initialdir=eval('self.ruta_{}'.format(r)),
                            mustexist=True)
        if ruta != '':
            if self.rutas_relativas.get():
                if r == 'datasets':
                    self.ruta_datasets = os.path.relpath(ruta)
                else:
                    self.ruta_resultados = os.path.relpath(ruta)
            else:
                if r == 'datasets':
                    self.ruta_datasets = ruta
                else:
                    self.ruta_resultados = ruta

    def cambia_rutas(self):
        if self.rutas_relativas.get():
            self.ruta_datasets = os.path.relpath(self.ruta_datasets)
            self.ruta_resultados = os.path.relpath(self.ruta_resultados)
        else:
            self.ruta_datasets = os.path.abspath(self.ruta_datasets)
            self.ruta_resultados = os.path.abspath(self.ruta_resultados)

    def limpia_atributos_y_clase(self):
        for i in self.tv_clase.get_children():
            self.tv_clase.delete(i)
        for i in self.tv_atributos.get_children():
            self.tv_atributos.delete(i)

    def limpia_muestra(self):
        self.t_muestra['state'] = 'normal'
        self.t_muestra.delete(1.0, 'end')
        self.t_muestra['state'] = 'disabled'

    def tamanyo_muestra(self, tamanyo):
        nuevo_tamanyo = askinteger('Muestra',
                                   '¿Cuántas evidencias quieres ver?',
                                   parent=self.root,
                                   minvalue=1,
                                   initialvalue=self._tamanyo_muestra)
        #                                            minvalue=0, maxvalue=1000)
        if nuevo_tamanyo:
            self._tamanyo_muestra = nuevo_tamanyo
        #TODO Debería averiguar el índice del menú que quiero modificar
        self.m_configuracion.entryconfigure(4, label='Tamaño muestra ({:,})'.\
                                                 format(self._tamanyo_muestra))
        self.escribe_muestra()

    #TODO Tratar excepciones para que no se quede habilitada la escritura
    def escribe_muestra(self):
        self.t_muestra['state'] = 'normal'
        self.t_muestra.delete(1.0, 'end')
        self.t_muestra.insert('end', '#######################################'\
                              '########################\n')
        self.t_muestra.insert('end', '   PRIMERAS {:,} LÍNEAS DEL DATASET DE '\
                              'CLASIFICACIÓN ORIGINAL\n'.\
                              format(self._tamanyo_muestra))
        self.t_muestra.insert('end', '#######################################'\
                              '########################\n')
        self.t_muestra.insert('end', self.dc.muestra(self._tamanyo_muestra))
        self.t_muestra.insert('end', '\n\n')
        self.t_muestra['state'] = 'disabled'

    def muestra_atributos_y_clase(self):
        self.limpia_atributos_y_clase()
        if self.v_tipo_dataset.get() == 'Dataset original':
            df = self.dc.info_dataset_original.columnas
        elif self.v_tipo_dataset.get() == 'Dataset sin evidencias incompletas':
            df = self.dc.info_dataset_sin_datos_desconocidos.columnas
        elif self.v_tipo_dataset.get() == 'Dataset sin atributos constantes':
            df = self.dc.info_dataset_sin_atributos_constantes.columnas
        elif self.v_tipo_dataset.get() == 'Catálogo':
            df = self.dc.info_catalogo.columnas
        elif self.v_tipo_dataset.get() == 'Catálogo Robusto':
            df = self.dc.info_catalogo_robusto.columnas

        self.tv_clase["columns"] = list(df.index).insert(0, 'Nombre')
        self.tv_atributos["columns"] = list(df.index).insert(0, 'Nombre')

        self.tv_clase.heading('Nombre', text='Nombre')
        self.tv_atributos.heading('Nombre', text='Nombre')
        for i in df.index:
            self.tv_clase.heading(i, text=i)
            self.tv_clase.column(i, minwidth=50, width=50, stretch=False)
            self.tv_atributos.heading(i, text=i)
            self.tv_atributos.column(i, minwidth=50, width=50, stretch=False)

        for i, atributo in enumerate(df.columns):
            valores = [valor if not pd.isnull(valor) else '-' for valor \
                       in df[atributo]]
            valores.insert(0, atributo)
            if atributo == self.dc.info_dataset_original.clase:
                self.tv_clase.insert('', 'end', text=(0), values=valores)
            else:
                self.tv_atributos.insert('',
                                         'end',
                                         text=(i + 1),
                                         values=valores)

    #TODO Modificar para que muestre información relevante de la celda.
    def selectItem(self, event):
        curItem = self.tv_atributos.item(self.tv_atributos.focus())
        col = self.tv_atributos.identify_column(event.x)
        print('curItem = ', curItem)
        print('col = ', col)
        if col == '#0':
            cell_value = curItem['text']
        else:
            cell_value = curItem['values'][int(col[1:]) - 1]
        print('cell_value = ', cell_value)

    def lee_configuracion(self):
        archivo_cfg = ConfigParser()
        archivo_cfg.optionxform = lambda option: option
        archivo_cfg.read('app.cfg')

        self.root.geometry(
            archivo_cfg.get('Ventana principal', 'Dimensiones y posición'))
        self.clase_al_final.set(
            archivo_cfg.get('Proyecto', 'Clase al final', fallback=True))
        self.rutas_relativas.set(
            archivo_cfg.get('Proyecto', 'Rutas relativas', fallback=True))
        self.ruta_datasets = archivo_cfg.get('Proyecto',
                                             'Ruta datasets',
                                             fallback='../datos/ACDC/')
        self.ruta_resultados = archivo_cfg.get('Proyecto',
                                               'Ruta resultados',
                                               fallback='../datos/catalogos/')

    def guarda_configuracion(self):
        archivo_cfg = ConfigParser()
        archivo_cfg.optionxform = lambda option: option

        #Dimensiones y posición de la ventana
        self.root.update()
        ancho, alto = self.root.winfo_width(), self.root.winfo_height()
        x, y = self.root.winfo_x(), self.root.winfo_y()
        archivo_cfg['Ventana principal'] = {\
            'Dimensiones y posición': '{}x{}+{}+{}'.format(ancho, alto, x, y)}

        archivo_cfg['Proyecto'] = {\
            'Clase al final': self.clase_al_final.get(),
            'Ruta datasets': self.ruta_datasets,
            'Ruta resultados': self.ruta_resultados,
            'Rutas relativas': self.rutas_relativas.get()}
        with open('app.cfg', 'w') as archivo:
            archivo_cfg.write(archivo)

    def cerrar_aplicacion(self):
        self.guarda_configuracion()
        self.root.destroy()
Exemple #13
0
class QueryBrowserUI(Tk):
    appName = "PySQLiTk3 - query browser"
    initSize = "900x600"
    configFile = 'conf.ini'
    sqlSqliteUrl = "http://www.sqlite.org/lang.html"
    tablesAtLeft = True
    selectedTable = ''
        
    def __init__(self, app):
        self.app = app
        super().__init__()
        self.configureLocale()
        self.title(self.appName)
        self.geometry(self.initSize)
        self.createMenu()
        self.createContent()
        self.loadLastDataBaseFile()
        
    def configureLocale(self):
        if sys.platform.startswith('win'):
            import locale
            if os.getenv('LANG') is None:
                lang, enc = locale.getdefaultlocale()
                os.environ['LANG'] = lang
        textdomain("app")
        bindtextdomain("app","./locale")

    def start(self):
        self.mainloop()
        
    def createMenu(self):
        menu = Menu(self)
        self.config(menu=menu)
        #File
        mnFile = Menu(menu, tearoff=0)
        menu.add_cascade(label=_("File"), underline=0, menu=mnFile)
        mnFile.add_command(label=_("New..."), underline=0, command=lambda cmd='NEW':  self.onCommand(cmd))
        mnFile.add_command(label=_("Open..."), underline=0, command=lambda cmd='OPEN': self.onCommand(cmd))
        mnFile.add_separator()
        mnFile.add_command(label=_("Quit"), underline=0, command=lambda cmd='EXIT': self.onCommand(cmd))
        #View
        self.mnView = Menu(menu, tearoff=0)
        menu.add_cascade(label=_("View"), underline=0, menu=self.mnView)
        self.mnView.add_command(label=_("Tables at %s") % _('right'), underline=0, command=lambda cmd='TABLES_RL': self.onCommand(cmd))
        #Help
        mnHelp = Menu(menu, tearoff=0)
        menu.add_cascade(label=_("Help"), underline=0, menu=mnHelp)
        mnHelp.add_command(label=_("SQL by SQLite..."), underline=0, command=lambda: webbrowser.open(self.sqlSqliteUrl))
        mnHelp.add_command(label=_("About..."), underline=0, command=lambda cmd='ABOUT': self.onCommand(cmd))
        
    def createContent(self):
    # Top Panel: ---------------------------------
        pnlTop = Frame(self)
        pnlTop.pack(side="top", fill="both", pady=10, padx=10)
        self.btnDB = Button(pnlTop, text=_("Open Data Base..."), command=lambda cmd='OPEN': self.onCommand(cmd))
        self.btnDB.grid(row=1, column=2, padx=5)

    # Query Panel: -----------------------------------
        self.pnlQuery = Frame(self)
        self.pnlQuery.pack(side="right", fill="both", expand=True, padx=10)
        #-- SQL Panel: 
        pnlSQL = Frame(self.pnlQuery)
        pnlSQL.pack(side="top", fill="both")
        Label(pnlSQL, text = "SQL:").grid(row=1, column=1, padx=5)
        self.txtSQL = TextSQL(pnlSQL, height=10, width = 60)
        self.txtSQL.grid(row=1, column=2, padx=5, pady=10)
        Button(pnlSQL, text = _("Run"), command=lambda cmd='RUN': self.onCommand(cmd)).grid(row=1, column=3, padx=5)
        #-- Buttons Panel
        pnlBtns = Frame(pnlSQL)
        pnlBtns.grid(row=2, column=2)
        Button(pnlBtns, text='INSERT', width = 12, command=lambda cmd='QUERY',query='INSERT': self.onCommand(cmd,query=query)).grid(row=1, column=1)
        Button(pnlBtns, text='SELECT', width = 12, command=lambda cmd='QUERY',query='SELECT': self.onCommand(cmd,query=query)).grid(row=1, column=2)
        Button(pnlBtns, text='UPDATE', width = 12, command=lambda cmd='QUERY',query='UPDATE': self.onCommand(cmd,query=query)).grid(row=1, column=3)
        Button(pnlBtns, text='DELETE', width = 12, command=lambda cmd='QUERY',query='DELETE': self.onCommand(cmd,query=query)).grid(row=1, column=4)        
        #-- Result Panel:
        self.pnlResult = Frame(self.pnlQuery)
        self.pnlResult.pack(side="top", fill="both", expand=True, pady=10, padx=10)
        self.resultGrid = TextResult(self.pnlResult)
        self.resultGrid.pack(side="top", fill="both", expand=True)
                
    #Table List Panel: ---------------------------------------
        self.pnlTables = Frame(self)
        self.pnlTables.pack(side="left", fill="both", pady=10, padx=10)
        #---Tables Buttons Panel: 
        self.pnlTableList = Frame(self.pnlTables)
        self.pnlTableList.pack(side="top", fill="both", pady=10)
        #---Panel Nueva: Button
        pnlNewTable = Frame(self.pnlTables)
        pnlNewTable.pack(side="bottom", pady=10, padx=10)
        Button(pnlNewTable, text=_("New Table"), command=lambda cmd='QUERY',query='CREATE': self.onCommand(cmd,query=query,table='<table>')).grid(row=1, column=2, padx=5)

    def tableContextMenu(self, event, table=''):
        popup = Menu(self, tearoff=0)
        popup.add_command(label=_("Add Column"), command=lambda cmd='QUERY',query='ADD', table=table: self.onCommand(cmd,query=query,table=table))
        popup.add_command(label=_("Rename Table"), command=lambda cmd='QUERY',query='RENAME', table=table: self.onCommand(cmd,query=query,table=table))
        popup.add_command(label=_("Drop Table"), command=lambda cmd='QUERY',query='DROP', table=table: self.onCommand(cmd,query=query,table=table))
        popup.post(event.x_root, event.y_root)

    def showTables(self):
        tables = self.app.getTables()
        self.pnlTableList.destroy()
        self.pnlTableList = Frame(self.pnlTables)
        self.pnlTableList.pack(side="top", fill="both", pady=10)
        self.btnTables = {}
        Label(self.pnlTableList, text=_('Tables')).grid(row=0, column=1)
        for n,table in enumerate(tables):
            self.btnTables[table] = Button(self.pnlTableList, text=table,width=20, command=lambda cmd='QUERY',query='SELECT',table=table: self.onCommand(cmd,query=query,table=table))
            self.btnTables[table].grid(row=n+1, column=1)
            self.btnTables[table].bind("<Button-3>", lambda event, table=table:self.tableContextMenu(event, table))
            if table == self.selectedTable:
                self.markTable(table)
        
    def onCommand(self, comando, **args):
        comandos = {
            'NEW':self.newDataBaseFile,
            'OPEN':self.openDataBaseFile,
            'EXIT':self.exitApp,
            'ABOUT':self.about,
            'QUERY':self.showQuery,
            'RUN':self.run,
            'TABLES_RL': self.showTablesAtRightOrLeft
        }
        try:
            comandos[comando](**args)
        except KeyError:
            showerror(_('Error'), _('Unknown command: ')+ comando)
            
    def exitApp(self):
        self.destroy()
        
    def about(self):
        showinfo(_('About...'), self.appName+'\n'+_('Desarrollado por:')+'\n'+_('Martín Nicolás Carbone')+'\n'+_('Agosto 2014'))

    def showTablesAtRightOrLeft(self):
        self.tablesAtLeft = not self.tablesAtLeft
        querySide, tablesSide = ("right", "left") if self.tablesAtLeft else ("left", "right")
        self.pnlTables.pack(side=tablesSide, fill="both", pady=10, padx=10)
        self.pnlQuery.pack(side=querySide, fill="both", expand=True, padx=10)
        self.mnView.entryconfigure(0, label=_("Tables at %s") % _(querySide))

    def selectTable(self, table):
        self.unMarkTable(self.selectedTable)
        self.selectedTable = table if table != '' else self.selectedTable
        self.markTable(self.selectedTable)
        return self.selectedTable

    def unMarkTable(self, table):
        try:
            self.btnTables[table].config(relief='raised')
        except KeyError:
            pass
    def markTable(self, table):
        try:
            self.btnTables[table].config(relief='sunken')
        except KeyError:
            pass
        
    def newDataBaseFile(self):
        path = asksaveasfilename(defaultextension=".db")
        if path != '':
            self.openDataBase(path)

    def openDataBaseFile(self):
        path = askopenfilename(filetypes=((_("Data base files"), "*.db;*.dat;*.sqlite;*.sqlite3;*.sql;"),(_("All files"), "*.*") ))
        if path != '':
            self.openDataBase(path)

    def loadLastDataBaseFile(self):
        path = self.readPath()
        if path != '':
            self.openDataBase(path) 
        
    def savePath(self, path):
        with open(self.configFile, 'w') as f:
            f.write(str(path))
        
    def readPath(self):
        try:
            with open(self.configFile, 'r') as f:
                return str(f.read())
        except IOError:
            return ''

    def openDataBase(self, path):
        self.basePath = path if path != '' else self.basePath
        try:
            self.app.openDataBase(self.basePath)
            self.savePath(self.basePath)
            self.btnDB.config(text=self.basePath)
            self.selectedTable = ''
            self.showTables()
        except IOError:
            showerror(_('Error'),_('Error')+' '+_("Open Data Base..."))

    def showQuery(self, query, table=''):
        table = self.selectTable(table)       
        sql=self.app.createQuery(query, table)
        self.txtSQL.setText(sql)

    def run(self):
        query = self.txtSQL.getText()
        result = self.app.runQuery(query)
        self.resultGrid.setText(result)
        if result == '':
            self.showTables()
Exemple #14
0
class Main(Tk):
    """
    Just edit Main()
    """
    open_file: object

    def __init__(self):
        super().__init__()
        self.fish = ()
        self.base_memory = [1024, "+"]
        self._create_window()

    def _create_window(self):
        # Root configure
        self.wm_resizable(False, False)
        self.wm_geometry("780x800")
        self.configure(bg="#3C3F41")

        # Frame
        self.top_control = Frame(self)
        self.left_hand_frame = Frame(self)
        self.right_hand_frame = Frame(self)

        self.top_control.pack(side=TOP)
        self.left_hand_frame.pack(side=LEFT)
        self.right_hand_frame.pack(side=RIGHT)

        # Scrollbar
        self.scrollbar = Scrollbar(self.left_hand_frame, orient=VERTICAL, command=self.on_scrollbar)
        self.scrollbar_d = Scrollbar(self.right_hand_frame, orient=VERTICAL, command=self.on_scrollbar)

        self.scrollbar.pack(side=RIGHT, fill=Y)
        self.scrollbar_d.pack(side=RIGHT, fill=Y)

        # Menu
        self.menu = Menu(self)
        self.configure(menu=self.menu)

        self.file_menu = Menu(self, tearoff=0)
        self.file_menu.add_command(label="Open", command=self.menu_open)
        self.file_menu.add_command(label="Save", command=self.menu_save, state=DISABLED)
        self.file_menu.add_command(label="Save as")

        self.menu.add_cascade(label="File", menu=self.file_menu)

        # Text lines
        self.text = Text(self.left_hand_frame, bg="#2B2B2B", width=47, fg="white", height=32, relief="flat",
                         yscrollcommand=self.scroll_func)
        self.text.insert(END,
                        "12 34 56 78 90 as df gh jk ls 12 gh 5t e4 gt ui"+"\n")
        self.text.insert(END,"as df gh jk lpn1 v2 h4 6j o9 u7 ff 34 "
                         "9s 99")
        self.text_hex = Text(self.right_hand_frame, bg="#2B2B2B", fg="white", width=16, height=32, relief="flat",
                             yscrollcommand=self.scroll_func)
        self.text_hex.insert(END, "12345\n12345\n12345\n12345")

        self.text.pack(side=LEFT)
        self.text_hex.pack(side=RIGHT)

        # Control panel
        self.svar = StringVar()
        self.ivar = IntVar()
        self.ivar.set(1024)
        self.fr = Radiobutton(self.top_control, text="512", variable=self.ivar,
                              value=512, command=self.change_increment)
        self.fr2 = Radiobutton(self.top_control, text="1024", variable=self.ivar,
                               value=1024, command=self.change_increment)

        self.fr.pack(side=RIGHT)
        self.fr2.pack(side=RIGHT, padx=5)

        self.spin = Spinbox(self.top_control, from_=0, textvariable=self.svar, increment=1024, to=10000,
                            wrap=True, command=self.draw, state=DISABLED)  # readonly
        self.spin.set(1024)
        self.spin_encoding = Spinbox(self.top_control, value=ENCODINGS, command=self.set_encoding)
        self.spin_encoding.set("utf-8")
        self.spin_encoding.pack()
        self.spin.pack()

        # Func
        self.bind_all("q", lambda event=None: print(self.text.get(0.0, END)))
        self._copy()
        self._opposite_copy()

    def set_encoding(self):
        self.file.encoding = self.spin_encoding.get()

    def menu_open(self):
        """we should use repr, not decode"""
        self.text.delete(0.0, END)
        self.text_hex.delete(0.0, END)

        self.open_file = askopenfilename(filetypes=(("eHex File", "*.eHex"),
                                                    ("All files", "*.*")))
        if self.open_file == '':
            pass
        else:
            self.spin.configure(state="readonly")
            self.file_menu.entryconfigure("Save", state=NORMAL)
            openfile = self.open_file
            print(os.path.getsize(self.open_file) if os.path.getsize(self.open_file) > 1024 else 1024)
            self.spin["to"] = os.path.getsize(self.open_file) if os.path.getsize(self.open_file) > 1024 else 1024

            self.file = FileEngine(self.open_file, "rb", encoding=self.spin_encoding.get())
            try:
                insert = self.file.read_sth("+", 1024)
            except AttributeError:
                insert = self.file.read_as_new("+", 1024)
            print("-start", insert)
            for i in insert[0]:
                print(type(i),i, " ".join(i)+"!", bs.b16decode("".join(i).encode()))
                self.text.insert(END, " ".join(i)+"\n")
            for i in insert[1]:
                print(type(i), i)
                self.text_hex.insert(END,i+"\n")
            print("hi")

    def menu_save(self):
        if savefilename is None:
            self.save_file = asksaveasfile(filetypes=(("eHex File", "*.eHex"),
                                                      ("All files", "*.*")))

            self.ob_file = FileEngine(self.save_file, "rb", openfile, encoding=self.spin_encoding.get())
            self.ob_file.write_sth()
            self.ob_file.close()

    def change_increment(self):
        self.spin["increment"] = self.ivar.get()

    def draw(self):
        self.text.delete(0.0, END)
        self.text_hex.delete(0.0, END)
        var = 0

        if int(self.spin.get()) < self.base_memory[0]:
            self.base_memory[0] = int(self.spin.get())
            self.base_memory[1] = "-"
            var = 0
            try:
                self.pre_draw = self.file.read_sth("-", int(self.spin.get()))
            except AttributeError:
                self.pre_draw = self.file.read_as_new("-", int(self.spin.get()))

            for i in self.pre_draw[0]:
                self.text.insert(END, i+'\n')
                var += 1
            var = 0
            for i in self.pre_draw[1]:
                self.text_hex.insert(END, i+'\n')
                var += 1

        elif int(self.spin.get()) > self.base_memory[0]:
            self.base_memory[0] = int(self.spin.get())
            self.base_memory[1] = "+"
            var = 0
            try:
                self.pre_draw = self.file.read_sth("+", int(self.spin.get()))
            except AttributeError:
                self.pre_draw = self.file.read_as_new("+", int(self.spin.get()))

            for i in self.pre_draw[0]:
                self.text.insert(END, " ".join(i)+'\n')
                var += 1
            var = 0
            for i in self.pre_draw[1]:
                self.text_hex.insert(END, i+'\n')
                var += 1

        print(self.base_memory, self.pre_draw)

    def scroll_func(self, *args):
        self.scrollbar.set(*args)
        self.scrollbar_d.set(*args)
        self.on_scrollbar('moveto', args[0])

    def on_scrollbar(self, *args):
        """Scrolls both text widgets when the scrollbar is moved"""
        self.text.yview(*args)
        self.text_hex.yview(*args)

    def _copy(self):
        self.after(20, self._copy)
        try:
            try:
                self.text_hex.tag_configure("selle", background="white", foreground="black")
                self.text_hex.tag_delete("selle")
            except TclError:
                pass
            self.first = self.text.index(SEL_FIRST).split(".")
            self.second = self.text.index(SEL_LAST).split(".")
            # print(self.first, self.second)

            self.first = (self.first[0],
                          str(self._pround(
                              (int(
                                  self.first[1]) + (
                                   1 if (int(self.first[1]) + 1) % 3 == 0 else 0)
                               ) / 3)))
            self.second = (self.second[0],
                           str(self._pround(
                               (int(
                                   self.second[1]) - (
                                    1 if (int(self.second[1]) - 1) % 3 == 0 else 0)
                                ) / 3)))

            # print("after", self.first, self.second)
            self.marks = (".".join(self.first), ".".join(self.second))
            # print(self.marks)
            self.text_hex.tag_add("selle", self.marks[0], self.marks[1])
            self.text_hex.tag_configure("selle", background=rgb(162, 8, 229), foreground="#ffffff")
            self.update_idletasks()
        except TclError:
            pass

    @staticmethod
    def _pround(r):
        if r > int(r):
            # print(r, int(r+1))
            return int(r) + 1
        else:
            # print(r, int(r), "else")
            return int(r)

    def _opposite_copy(self):
        self.after(20, self._opposite_copy)
        try:
            try:
                self.text.tag_configure("selle-hex", background="white", foreground="black")
                self.text.tag_delete("selle-hex")
            except TclError:
                pass
            self.hex_first, self.hex_second = self.text_hex.index(SEL_FIRST).split(".")[0] + "." + \
                                              str(int(self.text_hex.index(SEL_FIRST).split(".")[1]) * 3), \
                                              self.text_hex.index(SEL_LAST).split(".")[0] + "." + \
                                              str(int(self.text_hex.index(SEL_LAST).split(".")[1]) * 3 - 1)

            self.text.tag_add("selle-hex", self.hex_first, self.hex_second)
            self.text.tag_configure("selle-hex", background=rgb(229, 139, 8), foreground="#ffffff")
            self.update_idletasks()

        except TclError:
            pass
Exemple #15
0
class PlayerMenu(Menu):
    root: "GUI"
    movemenu: Menu
    axis: str
    startX: int
    startY: int
    cX: int
    cY: int
    tw: Toplevel

    def __init__(self, root: "GUI"):
        self.root = root
        Menu.__init__(self,
                      master=root,
                      tearoff=0)
        # create 'move' menu
        self.movemenu = Menu(master=self,
                             tearoff=0)
        self.movemenu.add_command(label="Move (Horizontal)",
                                  command=lambda: self.movePlayer("X"))
        self.movemenu.add_command(label="Move (Vertical)",
                                  command=lambda: self.movePlayer("Y"))
        self.movemenu.add_separator()
        self.movemenu.add_command(label="Save Position",
                                  command=self.savePos,
                                  state='disabled')
        self.movemenu.add_command(label="Reset Position",
                                  command=self.resetPos,
                                  state='disabled')
        # create 'main' menu
        self.add_cascade(label="Move Player",
                         menu=self.movemenu)
        self.add_separator()
        self.add_command(label="Exit Spotify",
                         command=lambda: root.spotify.win.close())
        self.add_command(label="Close Player",
                         command=root.closePlayer)

    def show(self, event: "Event") -> None:
        self.post(event.x_root, self.root.winfo_rooty())

    def movePlayer(self, axis: str) -> None:
        self.root.event_generate(sequence='<Motion>',
                                 warp=True,
                                 x=(self.root.screen.appW // 2),
                                 y=(self.root.screen.appH // 2))
        self.axis = axis
        cur = f"sb_{'h' if axis == 'X' else 'v'}_double_arrow"
        self.root.configure(cursor=cur)
        self.startX = self.root.winfo_rootx()
        self.startY = self.root.winfo_rooty()
        for btn in self.root.btns.values():
            btn.unbind(sequence='<ButtonRelease-1>')
        self.root.unbind_all(sequence='<ButtonRelease-3>')
        self.root.bind(sequence='<Escape>',
                       func=self.stopMoving)
        self.root.bind(sequence='<ButtonRelease-3>',
                       func=self.stopMoving)
        self.root.bind(sequence='<Button-1>',
                       func=self.on_click)
        self.root.bind(sequence='<B1-Motion>',
                       func=self.on_move)
        self.root.bind(sequence='<ButtonRelease-1>',
                       func=self.on_release)

    def on_click(self, event: "Event") -> None:
        self.cX = event.x
        self.cY = event.y
        self.tw = Toplevel(master=self.root,
                           bg='white',
                           bd=3,
                           relief='groove')
        self.tw.attributes('-transparentcolor', 'white',
                           '-topmost', 1)
        self.tw.overrideredirect(1)
        self.tw.geometry(self.root.geometry())

    def on_move(self, event: "Event") -> None:
        if self.axis == "X":
            pos = (event.x - self.cX)
            dX = pos
            dY = 0
        else:
            pos = (event.y - self.cY)
            dX = 0
            dY = pos
        self.tw.geometry(f'+{self.startX + dX}'
                         f'+{self.startY + dY}')

    def on_release(self, *_) -> None:
        if self.tw.geometry() != self.root.geometry():
            self.root.geometry(self.tw.geometry())
            self.movemenu.entryconfigure(3, state='normal')
            self.movemenu.entryconfigure(4, state='normal')
        self.tw.destroy()
        self.stopMoving()

    def stopMoving(self, *_) -> None:
        self.root.bind_all(sequence='<ButtonRelease-3>',
                           func=self.show)
        self.root.unbind(sequence='<Button-1>')
        self.root.unbind(sequence='<B1-Motion>')
        self.root.unbind(sequence='<ButtonRelease-1>')
        self.root.unbind(sequence='<Escape>')
        self.root.configure(cursor="")
        for id, btn in enumerate(self.root.btns.values()):
            btn.bind(sequence='<ButtonRelease-1>',
                     func=lambda e, n=id: self.root.sendInput(e, n))

    def savePos(self) -> None:
        global OFFSET_X, OFFSET_Y
        self.movemenu.entryconfigure(3, state='disabled')
        self.movemenu.entryconfigure(4, state='disabled')
        curX = self.root.winfo_rootx()
        curY = self.root.winfo_rooty()
        OFFSET_X += (self.root.screen.X - curX)
        OFFSET_Y += (self.root.screen.Y - curY)
        cfg['Sizes']['horizontal_offset'] = OFFSET_X
        cfg['Sizes']['vertical_offset'] = OFFSET_Y
        with cfgfile.open('w') as f:
            cfg.write(f)
        self.root.screen = Screen()

    def resetPos(self) -> None:
        self.movemenu.entryconfigure(3, state='disabled')
        self.movemenu.entryconfigure(4, state='disabled')
        self.root.focus_set()
        self.root.geometry(f'+{self.root.screen.X}'
                           f'+{self.root.screen.Y}')
Exemple #16
0
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