Esempio n. 1
0
    def cmd_analyse_photo(self):
        # If pointer is defined just switch focus to the window
        if self.win_photo_an:
            self.win_photo_an.focus_force()
            return

        # Create window from class and save pointer
        path = filedialog.askdirectory(title=get_name("ask_dir_photo_an"))
        if path:
            try:
                # Create window from class and save pointer
                self.win_photo_an = WinPhotoAn(master=self.master, path=path)

            # This exception will be raised if user chooses folder without photo
            # and rejects suggestion to choose another folder
            except ValueError:
                return

            # Bind handler on destroying to clean up self class
            self.win_photo_an.bind("<Destroy>",
                                   self.handle_destroy_win_photo_an)
Esempio n. 2
0
    def analyze_photo_from_project(self, _=None):
        # TODO: show warning to user
        if self.win_photo_an:
            self.win_photo_an.destroy()

        try:
            # Create window from class and save pointer
            self.win_photo_an = WinPhotoAn(
                master=self.master,
                path=os_path.split(self.project_file)[0],
                project_keywords=self.project_dict["keywords"])

        # This exception will be raised if user chooses a project without photo
        except ValueError:
            messagebox.showerror(
                parent=self.master,
                title=get_name("title_error_no_photo_in_project"),
                message=get_name("text_error_no_photo_in_project"))
            return

        # Bind handler on destroying to clean up self class
        self.win_photo_an.bind("<Destroy>", self.handle_destroy_win_photo_an)
    def cmd_analyse_photo(self):
        # If pointer is defined just switch focus to the window
        if self.win_photo_an:
            self.win_photo_an.focus_force()
            return

        # Create window from class and save pointer
        path = filedialog.askdirectory(title=get_name("ask_dir_photo_an"))
        if path:
            try:
                # Create window from class and save pointer
                self.win_photo_an = WinPhotoAn(master=self.master, path=path)

            # This exception will be raised if user chooses folder without photo
            # and rejects suggestion to choose another folder
            except ValueError:
                return

            # Bind handler on destroying to clean up self class
            self.win_photo_an.bind("<Destroy>", self.handle_destroy_win_photo_an)
    def analyze_photo_from_project(self, _=None):
        # TODO: show warning to user
        if self.win_photo_an:
            self.win_photo_an.destroy()

        try:
            # Create window from class and save pointer
            self.win_photo_an = WinPhotoAn(master=self.master,
                                           path=os_path.split(self.project_file)[0],
                                           project_keywords=self.project_dict["keywords"])

        # This exception will be raised if user chooses a project without photo
        except ValueError:
            messagebox.showerror(parent=self.master,
                                 title=get_name("title_error_no_photo_in_project"),
                                 message=get_name("text_error_no_photo_in_project"))
            return

        # Bind handler on destroying to clean up self class
        self.win_photo_an.bind("<Destroy>", self.handle_destroy_win_photo_an)
class WinMain():
    def __init__(self, master=None):
        self.master = master

        self.master.bind('<Control-o>', lambda _: self.cmd_open_project())
        self.master.bind('<Control-q>', lambda _: self.cmd_close_project())
        self.master.bind('<Control-n>', lambda _: self.cmd_create_project())
        self.master.bind('<Control-v>', lambda _: self.cmd_view_proj())
        self.master.bind('<Control-c>', lambda _: self.cmd_settings())
        self.master.bind('<Control-s>', lambda _: self.cmd_save_photo())
        self.master.bind('<Control-a>', lambda _: self.cmd_analyse_photo())
        self.master.bind('<F1>', lambda _: self.cmd_help())
        self.master.bind('<Escape>', lambda _: self.master.destroy())

        self.is_project_under_edition = False

        # Initialize pointers to child windows with empty values
        self.win_epp = None
        self.win_settings = None
        self.win_photo_an = None
        self.win_view_proj = None

        self.project_file = None
        self.project_dict = None

        # Menu
        self.menubar = Menu(master)
        self.master.config(menu=self.menubar)

        # Projects menu
        self.menu_project = Menu(self.menubar, tearoff=0)
        self.menu_project.add_command(label=get_name("cmd_open_project"),
                                      command=self.cmd_open_project)

        self.menu_project.add_command(label=get_name("cmd_close_project"),
                                      command=self.cmd_close_project, state=DISABLED)
        self.menu_project.add_command(label=get_name("cmd_create_project"),
                                      command=self.cmd_create_project)
        self.menu_project.add_command(label=get_name("cmd_view_proj"),
                                      command=self.cmd_view_proj)
        self.menu_project.add_separator()

        self.menu_project.add_command(label=get_name("cmd_settings"),
                                      command=self.cmd_settings)
        self.menu_project.add_separator()
        self.menu_project.add_command(label=get_name("cmd_exit"), command=self.master.destroy)

        self.menubar.add_cascade(label=get_name("menu_project"),
                                 menu=self.menu_project)

        # Common operations
        self.menu_common_op = Menu(self.menubar, tearoff=0)
        self.menu_common_op.add_command(label=get_name("cmd_save_photo"),
                                        command=self.cmd_save_photo)
        self.menu_common_op.add_command(label=get_name("cmd_analyse_photo"),
                                        command=self.cmd_analyse_photo)
        self.menubar.add_cascade(label=get_name("menu_common_op"),
                                 menu=self.menu_common_op)

        # Help
        self.menu_help = Menu(self.menubar, tearoff=0)
        self.menu_help.add_command(label=get_name("cmd_help"),
                                   command=self.cmd_help)
        self.menu_help.add_separator()
        self.menu_help.add_command(label=get_name("cmd_about"),
                                   command=self.cmd_about)
        self.menubar.add_cascade(label=get_name("menu_help"),
                                 menu=self.menu_help)

        self.frame_welcome = None
        self.frame_project = None
        self.create_frame_welcome()

    def create_frame_welcome(self):
        self.frame_welcome = ttk.Frame(master=self.master)
        self.frame_default_controls = ttk.Frame(master=self.frame_welcome)

        self.btn_open_proj = ttk.Button(master=self.frame_default_controls, text=get_name("btn_open_project"))
        self.btn_create_proj = ttk.Button(master=self.frame_default_controls, text=get_name("btn_create_project"))
        self.btn_view_proj = ttk.Button(master=self.frame_default_controls, text=get_name("btn_view_proj"))
        self.btn_save_photo = ttk.Button(master=self.frame_default_controls, text=get_name("btn_save_photo"))
        self.btn_analyse_photo = ttk.Button(master=self.frame_default_controls, text=get_name("btn_analyse_photo"))

        self.btn_open_proj.bind('<ButtonRelease-1>', lambda _: self.cmd_open_project())
        self.btn_create_proj.bind('<ButtonRelease-1>', lambda _: self.cmd_create_project())
        self.btn_view_proj.bind('<ButtonRelease-1>', lambda _: self.cmd_view_proj())
        self.btn_save_photo.bind('<ButtonRelease-1>', lambda _: self.cmd_save_photo())
        self.btn_analyse_photo.bind('<ButtonRelease-1>', lambda _: self.cmd_analyse_photo())

        self.lbl_welcome1 = ttk.Label(master=self.frame_welcome,
                                      text=get_name("welcome_text1"),
                                      justify=CENTER,
                                      padding=10,
                                      font=["Segoe Print", 16])


        self.lbl_welcome2 = ttk.Label(master=self.frame_welcome,
                                      text=get_name("welcome_text2"),
                                      justify=CENTER,
                                      padding=10,
                                      font=["Segoe Print", 12])

        self.frame_welcome.pack(fill=BOTH)
        self.lbl_welcome1.grid(row=0, column=0)
        self.lbl_welcome2.grid(row=1, column=0)
        self.frame_default_controls.grid(rowspan=2, row=0, column=1)
        self.btn_open_proj.pack(fill=X)
        self.btn_create_proj.pack(fill=X)
        self.btn_view_proj.pack(fill=X)
        self.btn_save_photo.pack(fill=X)
        self.btn_analyse_photo.pack(fill=X)

    def project_selected(self):
        if self.frame_welcome is not None:
            self.frame_welcome.destroy()
            self.frame_welcome = None

        self.menu_project.entryconfig(1, state=ACTIVE)

        with open(self.project_file, encoding='utf-8') as f:
            self.project_dict = json_load(f)

        if self.frame_project is not None:
            self.frame_project.destroy()


        self.frame_project = ttk.Frame(master=self.master)

        self.frame_proj_info = ttk.LabelFrame(master=self.frame_project, text=get_name("frame_proj_info"))

        text = '''{0}:\t"{1}"

{2}:\t{3}\t{4}
{5}:\t{6}\t{7}

{8}:
{9}

{10}:
{11}'''.format(get_name('name'),
               self.project_dict['name'],
               get_name('start'),
               self.project_dict['timeslot']['start']['time'],
               self.project_dict['timeslot']['start']['date'],
               get_name('finish'),
               self.project_dict['timeslot']['finish']['time'],
               self.project_dict['timeslot']['finish']['date'],
               get_name('keywords'),
               self.project_dict['keywords'] if self.project_dict['keywords'] else get_name("empty"),
               get_name('description'),
               self.project_dict['description'] if self.project_dict['description'].strip() else get_name("empty"))

        self.lbl_proj_info = ttk.Label(master=self.frame_proj_info,
                                       justify=LEFT,
                                       wraplength=450,
                                       text=text)

        self.frame_proj_controls = ttk.LabelFrame(master=self.frame_project, text=get_name("frame_proj_controls"))
        self.btn_analyze_photo = ttk.Button(master=self.frame_proj_controls, text=get_name("btn_analyze_photo"))
        self.btn_edit = ttk.Button(master=self.frame_proj_controls, text=get_name("btn_edit"))
        self.btn_close_proj = ttk.Button(master=self.frame_proj_controls, text=get_name("btn_close_proj"))
        self.btn_delete_proj = ttk.Button(master=self.frame_proj_controls, text=get_name("btn_delete_proj"))
        self.btn_refresh = ttk.Button(master=self.frame_proj_controls, text=get_name("btn_refresh"))

        self.btn_analyze_photo.bind('<ButtonRelease-1>', self.analyze_photo_from_project)
        self.btn_edit.bind('<ButtonRelease-1>', self.edit_project)
        self.btn_close_proj.bind('<ButtonRelease-1>', lambda _: self.cmd_close_project())
        self.btn_delete_proj.bind('<ButtonRelease-1>', lambda _: self.delete_proj())
        self.btn_refresh.bind('<ButtonRelease-1>', self.refresh)

        self.frame_proj_stat = ttk.LabelFrame(master=self.frame_project, text=get_name("frame_proj_stat"))

        proj_path = os_path.split(self.project_file)[0]
        folders = next(os_walk(proj_path))[1]

        if folders:
            # Prepare table with statistics about folders and files
            # ----------------------------------------------------------------------------------------------------------
            self.tree_folders = ttk.Treeview(master=self.frame_proj_stat,
                                             columns=('files', 'nested_folders'),
                                             height=len(folders),
                                             selectmode=NONE)
            self.tree_folders.column('#0', stretch=False, width=145)
            self.tree_folders.heading('#0', text=get_name('folder'))
            self.tree_folders.column('files', stretch=False, width=145)
            self.tree_folders.heading('files', text=get_name('files'))
            self.tree_folders.column('nested_folders', stretch=False, width=190)
            self.tree_folders.heading('nested_folders', text=get_name('nested_folders'))

            for ix, folder in enumerate(folders, start=1):
                self.tree_folders.insert('', 'end', ix, text=folder)
                self.tree_folders.set(ix, 'files', len(next(os_walk(os_path.join(proj_path, folder)))[2]))
                self.tree_folders.set(ix, 'nested_folders', len(next(os_walk(os_path.join(proj_path, folder)))[1]))
            # ==========================================================================================================

            # Prepare table with statistics about photographs basing on source photographs
            # ----------------------------------------------------------------------------------------------------------
            self.tree_source = ttk.Treeview(master=self.frame_proj_stat,
                                            height=10,
                                            selectmode=NONE,
                                            columns=("xmp", "fullsize", "monitor", "web", "panorama", "layered"))

            self.scroll_tree_y = ttk.Scrollbar(master=self.frame_proj_stat, orient='vertical', command=self.tree_source.yview)
            self.tree_source.configure(yscroll=self.scroll_tree_y.set)

            source_files = []
            xmp_files = []
            xmp_files_num = 0
            fs_files_num = 0
            mon_files_num = 0
            web_files_num = 0
            pan_files_num = 0
            layered_files_num = 0

            for file in next(os_walk((os_path.join(proj_path, dir_source))))[2]:
                if os_path.splitext(file)[-1].lower() in supported_image_ext:
                    source_files.append(file)
                if os_path.splitext(file)[-1].lower() == xmp_ext:
                    xmp_files.append(file)

            for source_file in source_files:
                fn_without_ext = os_path.splitext(source_file)[0]
                self.tree_source.insert('', 'end', fn_without_ext, text=source_file)
                for file in xmp_files:
                    if os_path.splitext(file)[0] == fn_without_ext:
                        xmp_files_num += 1
                        self.tree_source.set(fn_without_ext, 'xmp', '+')
                        break

                if os_path.isdir(os_path.join(proj_path, dir_fullsize)):
                    for file in next(os_walk((os_path.join(proj_path, dir_fullsize))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(1) == fn_without_ext:
                            fs_files_num += 1
                            self.tree_source.set(fn_without_ext, 'fullsize', '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_monitor)):
                    for file in next(os_walk((os_path.join(proj_path, dir_monitor))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(1) == fn_without_ext:
                            mon_files_num += 1
                            self.tree_source.set(fn_without_ext, 'monitor', '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_web)):
                    for file in next(os_walk((os_path.join(proj_path, dir_web))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(1) == fn_without_ext:
                            web_files_num += 1
                            self.tree_source.set(fn_without_ext, 'web', '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_panorama)):
                    for file in next(os_walk((os_path.join(proj_path, dir_panorama))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(1) == fn_without_ext:
                            pan_files_num += 1
                            self.tree_source.set(fn_without_ext, 'panorama', '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_layered)):
                    for file in next(os_walk((os_path.join(proj_path, dir_layered))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(1) == fn_without_ext:
                            layered_files_num += 1
                            self.tree_source.set(fn_without_ext, 'layered', '+')
                            break

            text = """{13}
({14} - {0}):
{15}\t\t{16}\t{17}
XMP:\t\t{1}\t\t\t{2}%
Fullsize:\t\t{3}\t\t\t{4}%
Monitor:\t\t{5}\t\t\t{6}%
Web:\t\t{7}\t\t\t{8}%
Panorama:\t{9}\t\t\t{10}%
Layered:\t\t{11}\t\t\t{12}%""".format(len(source_files),
                                      xmp_files_num,
                                      int(xmp_files_num / len(source_files) * 100),
                                      fs_files_num,
                                      int(fs_files_num / len(source_files) * 100),
                                      mon_files_num,
                                      int(mon_files_num / len(source_files) * 100),
                                      web_files_num,
                                      int(web_files_num / len(source_files) * 100),
                                      pan_files_num,
                                      int(pan_files_num / len(source_files) * 100),
                                      layered_files_num,
                                      int(layered_files_num / len(source_files) * 100),
                                      get_name("stat_of_edited"),
                                      get_name("source_files"),
                                      get_name("type"),
                                      get_name("num_of_files"),
                                      get_name("percent_from_source"))

            self.lbl_source_stat = ttk.Label(master=self.frame_proj_stat, text=text)

            self.tree_source.heading('#0', text='Source')
            self.tree_source.heading('xmp', text='XMP')
            self.tree_source.heading('fullsize', text='FS')
            self.tree_source.heading('monitor', text='Mon')
            self.tree_source.heading('web', text='Web')
            self.tree_source.heading('panorama', text='Pan')
            self.tree_source.heading('layered', text='Lrd')

            self.tree_source.column('#0', stretch=False, width=170)
            self.tree_source.column('xmp', stretch=False, width=50)
            self.tree_source.column('fullsize', stretch=False, width=50)
            self.tree_source.column('monitor', stretch=False, width=50)
            self.tree_source.column('web', stretch=False, width=50)
            self.tree_source.column('panorama', stretch=False, width=50)
            self.tree_source.column('layered', stretch=False, width=50)

        else:
            self.lbl_no_st_empty_prj = ttk.Label(master=self.frame_proj_stat, text=get_name("lbl_no_st_empty_prj"))

        self.frame_project.pack(fill=BOTH)
        self.frame_proj_info.grid(row=0, column=0, sticky=W + E + N + S)
        self.lbl_proj_info.pack(fill=X)
        self.frame_proj_controls.grid(row=1, column=0, sticky=W + E + N + S)
        self.btn_analyze_photo.pack(fill=X)
        self.btn_edit.pack(fill=X)
        self.btn_close_proj.pack(fill=X)
        self.btn_delete_proj.pack(fill=X)
        self.btn_refresh.pack(fill=X)
        self.frame_proj_stat.grid(row=0, column=1, rowspan=2, sticky=W + E + N + S)
        if folders:
            self.tree_folders.pack()
            self.lbl_source_stat.pack(fill=X)
            self.tree_source.pack(side=LEFT)
            self.scroll_tree_y.pack(side=RIGHT, fill=Y, expand=1)
        else:
            self.lbl_no_st_empty_prj.pack(fill=X)

    @staticmethod
    def get_numeric_parts_of_fn(filename):

        found_match = regex.match(filename)
        if found_match:
            print(found_match.group(1))
        exit(0)
        print(regex.match(filename).groups())
        print(regex.search(filename))
        numeric_parts = []
        print(os_path.splitext(filename)[0])
        normalized_fn = regex.sub(os_path.splitext(filename)[0], " ")
        # Try to get date/time from filename
        for name_part in normalized_fn.split():
            # Collect all numeric parts of filename
            if name_part.isnumeric():
                numeric_parts.append(name_part)
        return numeric_parts

    def analyze_photo_from_project(self, _=None):
        # TODO: show warning to user
        if self.win_photo_an:
            self.win_photo_an.destroy()

        try:
            # Create window from class and save pointer
            self.win_photo_an = WinPhotoAn(master=self.master,
                                           path=os_path.split(self.project_file)[0],
                                           project_keywords=self.project_dict["keywords"])

        # This exception will be raised if user chooses a project without photo
        except ValueError:
            messagebox.showerror(parent=self.master,
                                 title=get_name("title_error_no_photo_in_project"),
                                 message=get_name("text_error_no_photo_in_project"))
            return

        # Bind handler on destroying to clean up self class
        self.win_photo_an.bind("<Destroy>", self.handle_destroy_win_photo_an)

    def edit_project(self, _=None):
        # TODO: show warning to user
        if self.win_epp:
            self.win_epp.destroy()

        self.is_project_under_edition = True

        # Create window from class and save pointer
        self.win_epp = WinEpp(master=self.master, project_dict=self.project_dict)
        # Bind handler on destroying to clean up self class
        self.win_epp.bind("<Destroy>", self.handle_destroy_win_epp)

    def delete_proj(self, _=None):
        if messagebox.askyesno(parent=self.master, title=get_name("ask_conf_del_proj_title"), message=get_name("ask_conf_del_proj_text")):
            delete_folder(path=os_path.split(self.project_file)[0])
            self.cmd_close_project()

    def refresh(self, _=None):
        self.project_selected()

    def cmd_open_project(self):
        fn = filedialog.askopenfilename(parent=self.master,
                                        title=get_name("ask_project_file"),
                                        filetypes=[(get_name("photo_projects"),
                                                    "*.json")],
                                        initialdir=settings["projects_dir"])
        if fn:
            self.project_file = fn
            self.project_selected()

    def cmd_close_project(self):
        self.project_dict = None
        self.project_file = None
        self.menu_project.entryconfig(1, state=DISABLED)
        if self.frame_project is not None:
            self.frame_project.destroy()
            self.frame_project = None
            self.create_frame_welcome()

    def cmd_settings(self, _=None):
        # Create window from class and save pointer
        self.win_settings = WinSettings(master=self.master)
        # Bind handler on destroying to clean up self class
        self.win_settings.bind("<Destroy>", self.handle_destroy_win_settings)

    def handle_destroy_win_settings(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_epp:
            return

        # Unset pointer
        self.win_settings = None

    def cmd_create_project(self):
        # If pointer is defined just switch focus to the window
        if self.win_epp:
            self.win_epp.focus_force()
            return

        # Create window from class and save pointer
        self.win_epp = WinEpp(master=self.master)
        # Bind handler on destroying to clean up self class
        self.win_epp.bind("<Destroy>", self.handle_destroy_win_epp)

    def handle_destroy_win_epp(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_epp:
            return

        if self.is_project_under_edition:
            self.is_project_under_edition = False
            if self.win_epp.project_file is not None:
                self.project_file = self.win_epp.project_file
                self.project_dict = None
            self.project_selected()

        # Unset pointer
        self.win_epp = None

    def cmd_view_proj(self):
        if self.win_view_proj:
            self.win_view_proj.focus_force()
            return
        # Create window from class and save pointer
        self.win_view_proj = WinViewProj(master=self.master)
        # Bind handler on destroying to clean up self class
        self.win_view_proj.bind("<Destroy>", self.handle_destroy_win_view_proj)

    def handle_destroy_win_view_proj(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_view_proj:
            return

        if self.win_view_proj.selected_proj is not None:
            self.project_file = self.win_view_proj.selected_proj
            self.project_selected()

        # Unset pointer
        self.win_view_proj = None

    def cmd_save_photo(self):
        dir_with_photo = filedialog.askdirectory(parent=self.master, title=get_name("dia_save_photo"), initialdir='/')
        if not dir_with_photo:
            return
        if settings["save_photo"]["save_originals"] == "True":
            file_operation = shutil_copy2
        else:
            file_operation = shutil_move

        # Get list of files to save
        pho_for_saving_without_date = []
        ph_for_saving_with_date = []

        if settings["save_photo"]["check_unsorted"] == "True":
            # Check unsorted files
            files = next(os_walk(os_path.join(settings["projects_dir"], dir_unsorted)))[2]
            for file in files:
                if os_path.splitext(file)[-1].lower() in supported_image_ext:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching:
                            try:
                                # Convert collected numeric parts into datetime object
                                ph_for_saving_with_date.append([os_path.join(settings["projects_dir"],
                                                                             dir_unsorted,
                                                                             file),
                                                                datetime.strptime(str(found_matching.group(1)),
                                                                                  '%Y-%m-%d_%H-%M-%S'),
                                                                None])
                            except ValueError:
                                continue

        for root, _, files in os_walk(dir_with_photo):
            for file in files:
                if os_path.splitext(file)[-1].lower() in supported_image_ext:
                    try:
                        # Try to find date/time in metadata
                        possible_dt = et.get_data_from_image(os_path.join(root, file),
                                                             "-EXIF:DateTimeOriginal")["EXIF"]["DateTimeOriginal"]
                        # Convert collected numeric parts into datetime object
                        ph_for_saving_with_date.append([os_path.join(root,
                                                                     file),
                                                        datetime.strptime(possible_dt,
                                                                          '%Y:%m:%d %H:%M:%S'),
                                                        None])
                    # If date/time were not found in metadata too
                    except ValueError:
                        pho_for_saving_without_date.append(os_path.join(root, file))
                        continue

        # Connect photo and project basing on date/time
        for project in next(os_walk(settings["projects_dir"]))[1]:
            if not os_path.isfile(os_path.join(settings["projects_dir"], project, project_file)):
                continue

            with open(os_path.join(os_path.join(settings["projects_dir"]),
                                   project,
                                   project_file),
                      encoding='utf-8') as _f:
                pd = json_load(_f)

            # Parse project timeslot
            prj_start = '{0} {1}'.format(pd["timeslot"]["start"]["date"], pd["timeslot"]["start"]["time"])
            prj_start = datetime.strptime(prj_start, "%d.%m.%Y %H:%M")
            prj_finish = '{0} {1}'.format(pd["timeslot"]["finish"]["date"], pd["timeslot"]["finish"]["time"])
            prj_finish = datetime.strptime(prj_finish, "%d.%m.%Y %H:%M")

            for ph in ph_for_saving_with_date:
                if ph[2] is not None:
                    continue

                if prj_start <= ph[1] <= prj_finish:  # If photo date/time in project timeslot
                    ph[2] = os_path.join(settings["projects_dir"], project, dir_source)

        for ph in ph_for_saving_with_date:
            dest_dir = os_path.normpath(ph[2]) if ph[2] is not None else os_path.join(settings["projects_dir"],
                                                                                      dir_unsorted)
            # TODO: file renaming according to template YYYY-MM-DD_HH-MM-SS.ext
            if os_path.split(ph[0])[0] == dest_dir:
                trace.debug("Try to move photo to the same location: {0}".format(ph[0]))
            else:
                trace.debug("Save photo: {0} -> {1}".format(os_path.normpath(ph[0]), dest_dir))
                try:
                    # Copy/move image
                    file_operation(os_path.normpath(ph[0]), dest_dir)
                    # Copy/move XMP file too if it exists
                    if os_path.isfile(os_path.splitext(ph[0])[0] + xmp_ext):
                        file_operation(os_path.splitext(ph[0])[0] + xmp_ext, dest_dir)
                except shutil_Error as e:  # For example, if file already exists in destination directory
                    trace.warning(e)

    def cmd_analyse_photo(self):
        # If pointer is defined just switch focus to the window
        if self.win_photo_an:
            self.win_photo_an.focus_force()
            return

        # Create window from class and save pointer
        path = filedialog.askdirectory(title=get_name("ask_dir_photo_an"))
        if path:
            try:
                # Create window from class and save pointer
                self.win_photo_an = WinPhotoAn(master=self.master, path=path)

            # This exception will be raised if user chooses folder without photo
            # and rejects suggestion to choose another folder
            except ValueError:
                return

            # Bind handler on destroying to clean up self class
            self.win_photo_an.bind("<Destroy>", self.handle_destroy_win_photo_an)

    def handle_destroy_win_photo_an(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_photo_an:
            return

        # Unset pointer
        self.win_photo_an = None

    @staticmethod
    def cmd_help():
        #messagebox.showinfo(get_name("win_help"), get_name("text_help"))
        WinHelp(title=get_name("win_help"))

    @staticmethod
    def cmd_about():
        #messagebox.showinfo(get_name("win_about"), get_name("text_about"))
        WinAbout(title=get_name("win_about"), message=get_name("text_about"))
Esempio n. 6
0
class WinMain():
    def __init__(self, master=None):
        self.master = master

        self.master.bind('<Control-o>', lambda _: self.cmd_open_project())
        self.master.bind('<Control-q>', lambda _: self.cmd_close_project())
        self.master.bind('<Control-n>', lambda _: self.cmd_create_project())
        self.master.bind('<Control-v>', lambda _: self.cmd_view_proj())
        self.master.bind('<Control-c>', lambda _: self.cmd_settings())
        self.master.bind('<Control-s>', lambda _: self.cmd_save_photo())
        self.master.bind('<Control-a>', lambda _: self.cmd_analyse_photo())
        self.master.bind('<F1>', lambda _: self.cmd_help())
        self.master.bind('<Escape>', lambda _: self.master.destroy())

        self.is_project_under_edition = False

        # Initialize pointers to child windows with empty values
        self.win_epp = None
        self.win_settings = None
        self.win_photo_an = None
        self.win_view_proj = None

        self.project_file = None
        self.project_dict = None

        # Menu
        self.menubar = Menu(master)
        self.master.config(menu=self.menubar)

        # Projects menu
        self.menu_project = Menu(self.menubar, tearoff=0)
        self.menu_project.add_command(label=get_name("cmd_open_project"),
                                      command=self.cmd_open_project)

        self.menu_project.add_command(label=get_name("cmd_close_project"),
                                      command=self.cmd_close_project,
                                      state=DISABLED)
        self.menu_project.add_command(label=get_name("cmd_create_project"),
                                      command=self.cmd_create_project)
        self.menu_project.add_command(label=get_name("cmd_view_proj"),
                                      command=self.cmd_view_proj)
        self.menu_project.add_separator()

        self.menu_project.add_command(label=get_name("cmd_settings"),
                                      command=self.cmd_settings)
        self.menu_project.add_separator()
        self.menu_project.add_command(label=get_name("cmd_exit"),
                                      command=self.master.destroy)

        self.menubar.add_cascade(label=get_name("menu_project"),
                                 menu=self.menu_project)

        # Common operations
        self.menu_common_op = Menu(self.menubar, tearoff=0)
        self.menu_common_op.add_command(label=get_name("cmd_save_photo"),
                                        command=self.cmd_save_photo)
        self.menu_common_op.add_command(label=get_name("cmd_analyse_photo"),
                                        command=self.cmd_analyse_photo)
        self.menubar.add_cascade(label=get_name("menu_common_op"),
                                 menu=self.menu_common_op)

        # Help
        self.menu_help = Menu(self.menubar, tearoff=0)
        self.menu_help.add_command(label=get_name("cmd_help"),
                                   command=self.cmd_help)
        self.menu_help.add_separator()
        self.menu_help.add_command(label=get_name("cmd_about"),
                                   command=self.cmd_about)
        self.menubar.add_cascade(label=get_name("menu_help"),
                                 menu=self.menu_help)

        self.frame_welcome = None
        self.frame_project = None
        self.create_frame_welcome()

    def create_frame_welcome(self):
        self.frame_welcome = ttk.Frame(master=self.master)
        self.frame_default_controls = ttk.Frame(master=self.frame_welcome)

        self.btn_open_proj = ttk.Button(master=self.frame_default_controls,
                                        text=get_name("btn_open_project"))
        self.btn_create_proj = ttk.Button(master=self.frame_default_controls,
                                          text=get_name("btn_create_project"))
        self.btn_view_proj = ttk.Button(master=self.frame_default_controls,
                                        text=get_name("btn_view_proj"))
        self.btn_save_photo = ttk.Button(master=self.frame_default_controls,
                                         text=get_name("btn_save_photo"))
        self.btn_analyse_photo = ttk.Button(master=self.frame_default_controls,
                                            text=get_name("btn_analyse_photo"))

        self.btn_open_proj.bind('<ButtonRelease-1>',
                                lambda _: self.cmd_open_project())
        self.btn_create_proj.bind('<ButtonRelease-1>',
                                  lambda _: self.cmd_create_project())
        self.btn_view_proj.bind('<ButtonRelease-1>',
                                lambda _: self.cmd_view_proj())
        self.btn_save_photo.bind('<ButtonRelease-1>',
                                 lambda _: self.cmd_save_photo())
        self.btn_analyse_photo.bind('<ButtonRelease-1>',
                                    lambda _: self.cmd_analyse_photo())

        self.lbl_welcome1 = ttk.Label(master=self.frame_welcome,
                                      text=get_name("welcome_text1"),
                                      justify=CENTER,
                                      padding=10,
                                      font=["Segoe Print", 16])

        self.frame_welcome.pack(fill=BOTH)
        self.lbl_welcome1.grid(row=0, column=0)
        self.frame_default_controls.grid(rowspan=2, row=0, column=1)
        self.btn_open_proj.pack(fill=X)
        self.btn_create_proj.pack(fill=X)
        self.btn_view_proj.pack(fill=X)
        self.btn_save_photo.pack(fill=X)
        self.btn_analyse_photo.pack(fill=X)

    def project_selected(self):
        if self.frame_welcome is not None:
            self.frame_welcome.destroy()
            self.frame_welcome = None

        self.menu_project.entryconfig(1, state=ACTIVE)

        with open(self.project_file, encoding='utf-8') as f:
            self.project_dict = json_load(f)

        if self.frame_project is not None:
            self.frame_project.destroy()

        self.frame_project = ttk.Frame(master=self.master)

        self.frame_proj_info = ttk.LabelFrame(master=self.frame_project,
                                              text=get_name("frame_proj_info"))

        text = '''{0}:\t"{1}"

{2}:\t{3}\t{4}
{5}:\t{6}\t{7}

{8}:
{9}

{10}:
{11}'''.format(
            get_name('name'), self.project_dict['name'], get_name('start'),
            self.project_dict['timeslot']['start']['time'],
            self.project_dict['timeslot']['start']['date'], get_name('finish'),
            self.project_dict['timeslot']['finish']['time'],
            self.project_dict['timeslot']['finish']['date'],
            get_name('keywords'), self.project_dict['keywords']
            if self.project_dict['keywords'] else get_name("empty"),
            get_name('description'), self.project_dict['description']
            if self.project_dict['description'].strip() else get_name("empty"))

        self.lbl_proj_info = ttk.Label(master=self.frame_proj_info,
                                       justify=LEFT,
                                       wraplength=450,
                                       text=text)

        self.frame_proj_controls = ttk.LabelFrame(
            master=self.frame_project, text=get_name("frame_proj_controls"))
        self.btn_analyze_photo = ttk.Button(master=self.frame_proj_controls,
                                            text=get_name("btn_analyze_photo"))
        self.btn_edit = ttk.Button(master=self.frame_proj_controls,
                                   text=get_name("btn_edit"))
        self.btn_close_proj = ttk.Button(master=self.frame_proj_controls,
                                         text=get_name("btn_close_proj"))
        self.btn_delete_proj = ttk.Button(master=self.frame_proj_controls,
                                          text=get_name("btn_delete_proj"))
        self.btn_refresh = ttk.Button(master=self.frame_proj_controls,
                                      text=get_name("btn_refresh"))

        self.btn_analyze_photo.bind('<ButtonRelease-1>',
                                    self.analyze_photo_from_project)
        self.btn_edit.bind('<ButtonRelease-1>', self.edit_project)
        self.btn_close_proj.bind('<ButtonRelease-1>',
                                 lambda _: self.cmd_close_project())
        self.btn_delete_proj.bind('<ButtonRelease-1>',
                                  lambda _: self.delete_proj())
        self.btn_refresh.bind('<ButtonRelease-1>', self.refresh)

        self.frame_proj_stat = ttk.LabelFrame(master=self.frame_project,
                                              text=get_name("frame_proj_stat"))

        proj_path = os_path.split(self.project_file)[0]
        folders = next(os_walk(proj_path))[1]

        if folders:
            # Prepare table with statistics about folders and files
            # ----------------------------------------------------------------------------------------------------------
            self.tree_folders = ttk.Treeview(master=self.frame_proj_stat,
                                             columns=('files',
                                                      'nested_folders'),
                                             height=len(folders),
                                             selectmode=NONE)
            self.tree_folders.column('#0', stretch=False, width=145)
            self.tree_folders.heading('#0', text=get_name('folder'))
            self.tree_folders.column('files', stretch=False, width=145)
            self.tree_folders.heading('files', text=get_name('files'))
            self.tree_folders.column('nested_folders',
                                     stretch=False,
                                     width=190)
            self.tree_folders.heading('nested_folders',
                                      text=get_name('nested_folders'))

            for ix, folder in enumerate(folders, start=1):
                self.tree_folders.insert('', 'end', ix, text=folder)
                self.tree_folders.set(
                    ix, 'files',
                    len(next(os_walk(os_path.join(proj_path, folder)))[2]))
                self.tree_folders.set(
                    ix, 'nested_folders',
                    len(next(os_walk(os_path.join(proj_path, folder)))[1]))
            # ==========================================================================================================

            # Prepare table with statistics about photographs basing on source photographs
            # ----------------------------------------------------------------------------------------------------------
            self.tree_source = ttk.Treeview(master=self.frame_proj_stat,
                                            height=10,
                                            selectmode=NONE,
                                            columns=("xmp", "fullsize",
                                                     "monitor", "web",
                                                     "panorama", "layered"))

            self.scroll_tree_y = ttk.Scrollbar(master=self.frame_proj_stat,
                                               orient='vertical',
                                               command=self.tree_source.yview)
            self.tree_source.configure(yscroll=self.scroll_tree_y.set)

            source_files = []
            xmp_files = []
            xmp_files_num = 0
            fs_files_num = 0
            mon_files_num = 0
            web_files_num = 0
            pan_files_num = 0
            layered_files_num = 0

            for file in next(os_walk((os_path.join(proj_path,
                                                   dir_source))))[2]:
                if os_path.splitext(file)[-1].lower() in supported_image_ext:
                    source_files.append(file)
                if os_path.splitext(file)[-1].lower() == xmp_ext:
                    xmp_files.append(file)

            for source_file in source_files:
                fn_without_ext = os_path.splitext(source_file)[0]
                self.tree_source.insert('',
                                        'end',
                                        fn_without_ext,
                                        text=source_file)
                for file in xmp_files:
                    if os_path.splitext(file)[0] == fn_without_ext:
                        xmp_files_num += 1
                        self.tree_source.set(fn_without_ext, 'xmp', '+')
                        break

                if os_path.isdir(os_path.join(proj_path, dir_fullsize)):
                    for file in next(
                            os_walk((os_path.join(proj_path,
                                                  dir_fullsize))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(
                                1) == fn_without_ext:
                            fs_files_num += 1
                            self.tree_source.set(fn_without_ext, 'fullsize',
                                                 '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_monitor)):
                    for file in next(
                            os_walk((os_path.join(proj_path,
                                                  dir_monitor))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(
                                1) == fn_without_ext:
                            mon_files_num += 1
                            self.tree_source.set(fn_without_ext, 'monitor',
                                                 '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_web)):
                    for file in next(
                            os_walk((os_path.join(proj_path, dir_web))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(
                                1) == fn_without_ext:
                            web_files_num += 1
                            self.tree_source.set(fn_without_ext, 'web', '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_panorama)):
                    for file in next(
                            os_walk((os_path.join(proj_path,
                                                  dir_panorama))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(
                                1) == fn_without_ext:
                            pan_files_num += 1
                            self.tree_source.set(fn_without_ext, 'panorama',
                                                 '+')
                            break

                if os_path.isdir(os_path.join(proj_path, dir_layered)):
                    for file in next(
                            os_walk((os_path.join(proj_path,
                                                  dir_layered))))[2]:
                        found_matching = dt_in_fn_regex.match(file)
                        if found_matching and found_matching.group(
                                1) == fn_without_ext:
                            layered_files_num += 1
                            self.tree_source.set(fn_without_ext, 'layered',
                                                 '+')
                            break

            text = """{13}
({14} - {0}):
{15}\t\t{16}\t{17}
XMP:\t\t{1}\t\t\t{2}%
Fullsize:\t\t{3}\t\t\t{4}%
Monitor:\t\t{5}\t\t\t{6}%
Web:\t\t{7}\t\t\t{8}%
Panorama:\t{9}\t\t\t{10}%
Layered:\t\t{11}\t\t\t{12}%""".format(
                len(source_files), xmp_files_num,
                int(xmp_files_num / len(source_files) * 100), fs_files_num,
                int(fs_files_num / len(source_files) * 100), mon_files_num,
                int(mon_files_num / len(source_files) * 100), web_files_num,
                int(web_files_num / len(source_files) * 100), pan_files_num,
                int(pan_files_num / len(source_files) * 100),
                layered_files_num,
                int(layered_files_num / len(source_files) * 100),
                get_name("stat_of_edited"), get_name("source_files"),
                get_name("type"), get_name("num_of_files"),
                get_name("percent_from_source"))

            self.lbl_source_stat = ttk.Label(master=self.frame_proj_stat,
                                             text=text)

            self.tree_source.heading('#0', text='Source')
            self.tree_source.heading('xmp', text='XMP')
            self.tree_source.heading('fullsize', text='FS')
            self.tree_source.heading('monitor', text='Mon')
            self.tree_source.heading('web', text='Web')
            self.tree_source.heading('panorama', text='Pan')
            self.tree_source.heading('layered', text='Lrd')

            self.tree_source.column('#0', stretch=False, width=170)
            self.tree_source.column('xmp', stretch=False, width=50)
            self.tree_source.column('fullsize', stretch=False, width=50)
            self.tree_source.column('monitor', stretch=False, width=50)
            self.tree_source.column('web', stretch=False, width=50)
            self.tree_source.column('panorama', stretch=False, width=50)
            self.tree_source.column('layered', stretch=False, width=50)

        else:
            self.lbl_no_st_empty_prj = ttk.Label(
                master=self.frame_proj_stat,
                text=get_name("lbl_no_st_empty_prj"))

        self.frame_project.pack(fill=BOTH)
        self.frame_proj_info.grid(row=0, column=0, sticky=W + E + N + S)
        self.lbl_proj_info.pack(fill=X)
        self.frame_proj_controls.grid(row=1, column=0, sticky=W + E + N + S)
        self.btn_analyze_photo.pack(fill=X)
        self.btn_edit.pack(fill=X)
        self.btn_close_proj.pack(fill=X)
        self.btn_delete_proj.pack(fill=X)
        self.btn_refresh.pack(fill=X)
        self.frame_proj_stat.grid(row=0,
                                  column=1,
                                  rowspan=2,
                                  sticky=W + E + N + S)
        if folders:
            self.tree_folders.pack()
            self.lbl_source_stat.pack(fill=X)
            self.tree_source.pack(side=LEFT)
            self.scroll_tree_y.pack(side=RIGHT, fill=Y, expand=1)
        else:
            self.lbl_no_st_empty_prj.pack(fill=X)

    @staticmethod
    def get_numeric_parts_of_fn(filename):

        found_match = regex.match(filename)
        if found_match:
            print(found_match.group(1))
        exit(0)
        print(regex.match(filename).groups())
        print(regex.search(filename))
        numeric_parts = []
        print(os_path.splitext(filename)[0])
        normalized_fn = regex.sub(os_path.splitext(filename)[0], " ")
        # Try to get date/time from filename
        for name_part in normalized_fn.split():
            # Collect all numeric parts of filename
            if name_part.isnumeric():
                numeric_parts.append(name_part)
        return numeric_parts

    def analyze_photo_from_project(self, _=None):
        # TODO: show warning to user
        if self.win_photo_an:
            self.win_photo_an.destroy()

        try:
            # Create window from class and save pointer
            self.win_photo_an = WinPhotoAn(
                master=self.master,
                path=os_path.split(self.project_file)[0],
                project_keywords=self.project_dict["keywords"])

        # This exception will be raised if user chooses a project without photo
        except ValueError:
            messagebox.showerror(
                parent=self.master,
                title=get_name("title_error_no_photo_in_project"),
                message=get_name("text_error_no_photo_in_project"))
            return

        # Bind handler on destroying to clean up self class
        self.win_photo_an.bind("<Destroy>", self.handle_destroy_win_photo_an)

    def edit_project(self, _=None):
        # TODO: show warning to user
        if self.win_epp:
            self.win_epp.destroy()

        self.is_project_under_edition = True

        # Create window from class and save pointer
        self.win_epp = WinEpp(master=self.master,
                              project_dict=self.project_dict)
        # Bind handler on destroying to clean up self class
        self.win_epp.bind("<Destroy>", self.handle_destroy_win_epp)

    def delete_proj(self, _=None):
        if messagebox.askyesno(parent=self.master,
                               title=get_name("ask_conf_del_proj_title"),
                               message=get_name("ask_conf_del_proj_text")):
            delete_folder(path=os_path.split(self.project_file)[0])
            self.cmd_close_project()

    def refresh(self, _=None):
        self.project_selected()

    def cmd_open_project(self):
        fn = filedialog.askopenfilename(parent=self.master,
                                        title=get_name("ask_project_file"),
                                        filetypes=[(get_name("photo_projects"),
                                                    "*.json")],
                                        initialdir=settings["projects_dir"])
        if fn:
            self.project_file = fn
            self.project_selected()

    def cmd_close_project(self):
        self.project_dict = None
        self.project_file = None
        self.menu_project.entryconfig(1, state=DISABLED)
        if self.frame_project is not None:
            self.frame_project.destroy()
            self.frame_project = None
            self.create_frame_welcome()

    def cmd_settings(self, _=None):
        # Create window from class and save pointer
        self.win_settings = WinSettings(master=self.master)
        # Bind handler on destroying to clean up self class
        self.win_settings.bind("<Destroy>", self.handle_destroy_win_settings)

    def handle_destroy_win_settings(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_epp:
            return

        # Unset pointer
        self.win_settings = None

    def cmd_create_project(self):
        # If pointer is defined just switch focus to the window
        if self.win_epp:
            self.win_epp.focus_force()
            return

        # Create window from class and save pointer
        self.win_epp = WinEpp(master=self.master)
        # Bind handler on destroying to clean up self class
        self.win_epp.bind("<Destroy>", self.handle_destroy_win_epp)

    def handle_destroy_win_epp(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_epp:
            return

        if self.is_project_under_edition:
            self.is_project_under_edition = False
            if self.win_epp.project_file is not None:
                self.project_file = self.win_epp.project_file
                self.project_dict = None
            self.project_selected()

        # Unset pointer
        self.win_epp = None

    def cmd_view_proj(self):
        if self.win_view_proj:
            self.win_view_proj.focus_force()
            return
        # Create window from class and save pointer
        self.win_view_proj = WinViewProj(master=self.master)
        # Bind handler on destroying to clean up self class
        self.win_view_proj.bind("<Destroy>", self.handle_destroy_win_view_proj)

    def handle_destroy_win_view_proj(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_view_proj:
            return

        if self.win_view_proj.selected_proj is not None:
            self.project_file = self.win_view_proj.selected_proj
            self.project_selected()

        # Unset pointer
        self.win_view_proj = None

    def cmd_save_photo(self):
        dir_with_photo = filedialog.askdirectory(
            parent=self.master,
            title=get_name("dia_save_photo"),
            initialdir='/')
        if not dir_with_photo:
            return
        if settings["save_photo"]["save_originals"] == "True":
            file_operation = shutil_copy2
        else:
            file_operation = shutil_move

        # Get list of files to save
        pho_for_saving_without_date = []
        ph_for_saving_with_date = []

        if settings["save_photo"]["check_unsorted"] == "True":
            # Check unsorted files
            files = next(
                os_walk(os_path.join(settings["projects_dir"],
                                     dir_unsorted)))[2]
            for file in files:
                if os_path.splitext(file)[-1].lower() in supported_image_ext:
                    found_matching = dt_in_fn_regex.match(file)
                    if found_matching:
                        try:
                            # Convert collected numeric parts into datetime object
                            ph_for_saving_with_date.append([
                                os_path.join(settings["projects_dir"],
                                             dir_unsorted, file),
                                datetime.strptime(str(found_matching.group(1)),
                                                  '%Y-%m-%d_%H-%M-%S'), None
                            ])
                        except ValueError:
                            continue

        for root, _, files in os_walk(dir_with_photo):
            for file in files:
                if os_path.splitext(file)[-1].lower() in supported_image_ext:
                    try:
                        # Try to find date/time in metadata
                        possible_dt = et.get_data_from_image(
                            os_path.join(root, file), "-EXIF:DateTimeOriginal"
                        )["EXIF"]["DateTimeOriginal"]
                        # Convert collected numeric parts into datetime object
                        ph_for_saving_with_date.append([
                            os_path.join(root, file),
                            datetime.strptime(possible_dt,
                                              '%Y:%m:%d %H:%M:%S'), None
                        ])
                    # If date/time were not found in metadata too
                    except ValueError:
                        pho_for_saving_without_date.append(
                            os_path.join(root, file))
                        continue

        # Connect photo and project basing on date/time
        for project in next(os_walk(settings["projects_dir"]))[1]:
            if not os_path.isfile(
                    os_path.join(settings["projects_dir"], project,
                                 project_file)):
                continue

            with open(os_path.join(os_path.join(settings["projects_dir"]),
                                   project, project_file),
                      encoding='utf-8') as _f:
                pd = json_load(_f)

            # Parse project timeslot
            prj_start = '{0} {1}'.format(pd["timeslot"]["start"]["date"],
                                         pd["timeslot"]["start"]["time"])
            prj_start = datetime.strptime(prj_start, "%d.%m.%Y %H:%M")
            prj_finish = '{0} {1}'.format(pd["timeslot"]["finish"]["date"],
                                          pd["timeslot"]["finish"]["time"])
            prj_finish = datetime.strptime(prj_finish, "%d.%m.%Y %H:%M")

            for ph in ph_for_saving_with_date:
                if ph[2] is not None:
                    continue

                if prj_start <= ph[
                        1] <= prj_finish:  # If photo date/time in project timeslot
                    ph[2] = os_path.join(settings["projects_dir"], project,
                                         dir_source)

        for ph in ph_for_saving_with_date:
            dest_dir = os_path.normpath(
                ph[2]) if ph[2] is not None else os_path.join(
                    settings["projects_dir"], dir_unsorted)
            # TODO: file renaming according to template YYYY-MM-DD_HH-MM-SS.ext
            if os_path.split(ph[0])[0] == dest_dir:
                trace.debug(
                    "Try to move photo to the same location: {0}".format(
                        ph[0]))
            else:
                trace.debug("Save photo: {0} -> {1}".format(
                    os_path.normpath(ph[0]), dest_dir))
                try:
                    # Copy/move image
                    file_operation(os_path.normpath(ph[0]), dest_dir)
                    # Copy/move XMP file too if it exists
                    if os_path.isfile(os_path.splitext(ph[0])[0] + xmp_ext):
                        file_operation(
                            os_path.splitext(ph[0])[0] + xmp_ext, dest_dir)
                except shutil_Error as e:  # For example, if file already exists in destination directory
                    trace.warning(e)

    def cmd_analyse_photo(self):
        # If pointer is defined just switch focus to the window
        if self.win_photo_an:
            self.win_photo_an.focus_force()
            return

        # Create window from class and save pointer
        path = filedialog.askdirectory(title=get_name("ask_dir_photo_an"))
        if path:
            try:
                # Create window from class and save pointer
                self.win_photo_an = WinPhotoAn(master=self.master, path=path)

            # This exception will be raised if user chooses folder without photo
            # and rejects suggestion to choose another folder
            except ValueError:
                return

            # Bind handler on destroying to clean up self class
            self.win_photo_an.bind("<Destroy>",
                                   self.handle_destroy_win_photo_an)

    def handle_destroy_win_photo_an(self, ev=None):
        # Handle only destroying of main window
        if ev.widget != self.win_photo_an:
            return

        # Unset pointer
        self.win_photo_an = None

    @staticmethod
    def cmd_help():
        #messagebox.showinfo(get_name("win_help"), get_name("text_help"))
        WinHelp(title=get_name("win_help"))

    @staticmethod
    def cmd_about():
        #messagebox.showinfo(get_name("win_about"), get_name("text_about"))
        WinAbout(title=get_name("win_about"), message=get_name("text_about"))