#!/usr/bin/env python # -*- coding: utf-8 -*- """ NodeFinder: Do calibration or add Branch Label or add Clade Label. """ from __future__ import (with_statement, print_function) import os import re import sys import time import platform from subprocess import Popen, PIPE if sys.version[0] == '2': import Tkinter as tk import ttk import tkMessageBox import tkFileDialog import ScrolledText as st elif sys.version[0] == '3': import tkinter as tk from tkinter import ttk from tkinter import messagebox as tkMessageBox from tkinter import filedialog as tkFileDialog import tkinter.scrolledtext as st else: raise ImportError('Cannot identify your Python version.') __version__ = '0.5.0' __author__ = 'Jin' # =========================================================== # Options you may want to change # =========================================================== # The length of insertion position display. # For example, default value (20): # | -> 20 <- || -> 20 <- | # [Insert]: b,c)>0.05<0.07,(d,e)))>0.1<0.2,(f,g))>0. # [Insert]: ->||<- # [Insert]: Insert Here INSERT_POSITION_HALF_SIZE = 20 LOG_AREA_FOREGROUND = '#FDF6E3' LOG_AREA_BACKGROUND = '#002B36' # Use set(list()) rather than {} for Python2.6 compatibility NONE_TREE_NAME_SYMBOL_SET = set( [',', ';', ')', '"', "'", '#', '$', '@', '>', '<']) NO_CALI_EXISTS_SYMBOL_SET = set([',', ';', ')']) WITH_CALI_EXISTS_SYMBOL_SET = set( ['>', '<', '@', '0', '1', "'", '"', '$', ':']) NO_LABEL_EXISTS_SYMBOL_SET = set([',', ';', ')']) WITH_LABEL_EXISTS_SYMBOL_SET = set( ['>', '<', '@', '0', '1', "'", '"', '$', ':', '#']) WARNING_CALI_OR_LABEL_INFO_SYMBOL_SET = set( ['>', '<', '@', '#', '$', "'", '"', ':']) WARNING_BRANCH_LABEL_SYMBOL_SET = set(['@', '#', '$', "'", ':']) # Regular expression for finding all species names in Newick tree RE_FIND_ALL_SPECIES = r'[^(),;]+' # Character set which will not be the first char of a valid species name NON_SPECIES_NAME_STARTING_CHAR_SET = set(['#', ':', '>', '<', '/', '\\']) # Character set that may be in the middle of species name and a number NON_SPECIES_NAME_MIDDLE_CHAR_SET = set(['#', ':']) # =========================================================== # Options generally don't need to be changed # =========================================================== GUI_TITLE = "NodeFinder GUI" INIT_WINDOW_SIZE = '1200x700' FORMAT_STR_GUI_TITLE = ' %s (Ver %s)' FORMAT_STR_TIME = " %d %b %Y, %a %H:%M:%S" THIN_BAR = '~' * 50 LONG_BAR = '=' * 50 CURRENT_PLATFORM = platform.system() WINDOWS_PLATFORM = 'Windows' LINUX_PLATFORM = 'Linux' MAC_PLATFORM = 'Darwin' ABOUT = """ %s, GUI implementation of NodeFinder Version : %s URL (GUI): https://github.com/zxjsdp/NodeFinderGUI URL (C) : https://github.com/zxjsdp/NodeFinderC """ % (GUI_TITLE, __version__) DOCUMENTATION = """ Documentation of %s (Ver. %s) [Basic Usage] 1. Open Newick tree file 2. Input calibration configs 3. Press "Execute All" button to execute [Config Syntax] name_a, name_b, calibration_infomation_1 name_c, name_d, calibration_infomation_2 name_a, name_b, clade_label_information name, branch_label_information ..., ..., ... [Simple Example] Given a Newick tree like this: ((a ,((b, c), (d, e))), (f, g)); Use this calibration config (blanks are OK) (fake data): c, b, >0.05<0.07 a, e, >0.1<0.2 c, f, >0.3<0.5 d, e, $1 a, #1 We will get output tree like this: ((a #1 , ((b, c)>0.05<0.07, (d, e)$1))>0.1<0.2, (f, g))>0.3<0.5; Topology (ASCII style): +---------- a #1 | | >0.1<0.2 +---| +-- b | | +---| >0.05<0.07 | | | +-- c | +---| | | +-- d | +---| $1 ----|>0.3<0.5 +-- e | | +-- f +-----------| +-- g """ % (GUI_TITLE, __version__) HELP = ('\nIf you need help, please check the menu bar:\n\n' ' Help -> Documentation\n') EN_DICT = { "buttons": { "RIGHT_MENU_CUT": 'Cut', "RIGHT_MENU_COPY": 'Copy', "RIGHT_MENU_PASTE": 'Paste', "RIGHT_MENU_DELETE": 'Delete', "RIGHT_MENU_SELECT": 'Select all', "RIGHT_MENU_CLEAR": 'Clear all', "OPEN_TREE_BUTTON": "Open tree file...", "CLEAR_BUTTON": "Clear", "LOAD_HISTORY_BUTTON": 'Load History', "EXECUTE_ALL_BUTTON": 'Execute All', "READ_CONFIG_BUTTON": 'Read Config File...', "SAVE_CONFIG_BUTTON": 'Save Config to File...', "ADD_NEW_BUTTON": 'Add New', "VIEW_ASCII_BUTTON": 'View As ASCII', "QUICK_SAVE_BUTTON": 'Quick Save', "SAVE_TREE_AS_BUTTON": 'Save New Tree As...', "SAVE_LOG_AS_BUTTON": 'Save Log As...', }, "labels": { "TREE_PANE_LABEL": 'Origin Tree', "CONFIG_PANE_LABEL": 'Configuration', "OUTPUT_LABEL": 'Tree Output', "LOG_PANE": 'Results and Log', "NAME_A_LABEL": 'Name A', "NAME_B_LABEL": 'Name B', "INFO_LABEL": 'Info', "DISPLAY_WIDTH_LABEL": 'Display width', }, "menubar": { "MENUBAR_OPEN": 'Open input tree file...', "MENUBAR_SAVE_TO": 'Save output tree to file...', "MENUBAR_SAVE_LOG": 'Save log to file...', "MENUBAR_EXIT": 'Exit', "MENUBAR_FILE_CASCADE": 'File', "MENUBAR_OPEN_CONFIG": 'Open config file...', "MENUBAR_SAVE_CONFIG": 'Save config to file...', "MENUBAR_CONFIG_CASCADE": "Configs", "MENUBAR_CUT": "Cut", "MENUBAR_COPY": "Copy", "MENUBAR_PASTE": "Paste", "MENUBAR_DELETE": "Delete", "MENUBAR_EDIT_CASCADE": 'Edit', "MENUBAR_DOC": "Documentation", "MENUBAR_ABOUT": "About", "MENUBAR_HELP_CASCADE": "Help", }, "notice": { "CLEAR_ALL_NOTICE": 'Erase all text?', "NO_STR_IN_CLIPBOARD": 'No string in clipboard!', } } CN_DICT = { "buttons": { "RIGHT_MENU_CUT": '剪切', "RIGHT_MENU_COPY": '复制', "RIGHT_MENU_PASTE": '粘贴', "RIGHT_MENU_DELETE": '删除', "RIGHT_MENU_SELECT": '全选', "RIGHT_MENU_CLEAR": '全部清除', "OPEN_TREE_BUTTON": "打开树文件...", "CLEAR_BUTTON": "清空", "LOAD_HISTORY_BUTTON": '读取历史文件', "EXECUTE_ALL_BUTTON": '执行', "READ_CONFIG_BUTTON": '读取配置文件...', "SAVE_CONFIG_BUTTON": '配置文件另存为...', "ADD_NEW_BUTTON": '增加新配置行', "VIEW_ASCII_BUTTON": '使用ASCII方式查看', "QUICK_SAVE_BUTTON": '结果快速存储', "SAVE_TREE_AS_BUTTON": '结果另存为...', "SAVE_LOG_AS_BUTTON": 'Log另存为...', }, "labels": { "TREE_PANE_LABEL": '原始树窗口', "CONFIG_PANE_LABEL": '配置窗口', "OUTPUT_LABEL": '输出文件窗口', "LOG_PANE": '结果窗口', "NAME_A_LABEL": '物种名 A', "NAME_B_LABEL": '物种名 B', "INFO_LABEL": '标定信息', "DISPLAY_WIDTH_LABEL": '展示宽度', }, "menubar": { "MENUBAR_OPEN": '打开树文件...', "MENUBAR_SAVE_TO": '结果另存为...', "MENUBAR_SAVE_LOG": 'Log另存为...', "MENUBAR_EXIT": '退出', "MENUBAR_FILE_CASCADE": '文件', "MENUBAR_OPEN_CONFIG": '打开配置文件...', "MENUBAR_SAVE_CONFIG": '配置文件另存为...', "MENUBAR_CONFIG_CASCADE": "配置", "MENUBAR_CUT": "剪切", "MENUBAR_COPY": "复制", "MENUBAR_PASTE": "粘贴", "MENUBAR_DELETE": "删除", "MENUBAR_EDIT_CASCADE": '编辑', "MENUBAR_DOC": "帮助", "MENUBAR_ABOUT": "关于", "MENUBAR_HELP_CASCADE": "帮助", }, "notice": { "CLEAR_ALL_NOTICE": '清空所有?', "NO_STR_IN_CLIPBOARD": '剪切板中没有可粘贴内容!', } } TEXT_DICT = EN_DICT global_insertion_list_cache = {} def time_now(): """Return a formatted time string: Hour:Minute:Second.""" return time.strftime("%H:%M:%S", time.localtime()) class RightClickMenu(object): """ Simple widget to add basic right click menus to entry widgets. usage: rclickmenu = RightClickMenu(some_entry_widget) some_entry_widget.bind("<3>", rclickmenu) If you prefer to import Tkinter over Tix, just replace all Tix references with Tkinter and this will still work fine. """ def __init__(self, parent): self.parent = parent # bind Control-A to select_all() to the widget. All other # accelerators seem to work fine without binding such as # Ctrl-V, Ctrl-X, Ctrl-C etc. Ctrl-A was the only one I had # issue with. self.parent.bind("<Control-a>", lambda e: self._select_all(), add='+') self.parent.bind("<Control-A>", lambda e: self._select_all(), add='+') def __call__(self, event): # if the entry widget is disabled do nothing. if self.parent.cget('state') == 'disable': return # grab focus of the entry widget. this way you can see # the cursor and any marking selections self.parent.focus_force() self.build_menu(event) def build_menu(self, event): """Build right click menu""" menu = tk.Menu(self.parent, tearoff=0) # check to see if there is any marked text in the entry widget. # if not then Cut and Copy are disabled. if not self.parent.selection_present(): menu.add_command( label=TEXT_DICT.get('buttons').get("RIGHT_MENU_CUT"), state='disable') menu.add_command( label=TEXT_DICT.get('buttons').get("RIGHT_MENU_COPY"), state='disable') else: # use Tkinter's virtual events for brevity. These could # be hardcoded with our own functions to immitate the same # actions but there's no point except as a novice exercise # (which I recommend if you're a novice). menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_CUT"), command=self._cut) menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_COPY"), command=self._copy) # if there's string data in the clipboard then make the normal # Paste command. otherwise disable it. if self.paste_string_state(): menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_PASTE"), command=self._paste) else: menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_PASTE"), state='disable') # again, if there's no marked text then the Delete option is disabled. if not self.parent.selection_present(): menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_DELETE"), state='disable') else: menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_DELETE"), command=self._clear) # make things pretty with a horizontal separator menu.add_separator() # I don't know of if there's a virtual event for select all though # I did look in vain for documentation on -any- of Tkinter's # virtual events. Regardless, the method itself is trivial. menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_SELECT"), command=self._select_all) menu.post(event.x_root, event.y_root) def _cut(self): self.parent.event_generate("<<Cut>>") def _copy(self): self.parent.event_generate("<<Copy>>") def _paste(self): self.parent.event_generate("<<Paste>>") def _clear(self): self.parent.event_generate("<<Clear>>") def _select_all(self): self.parent.selection_range(0, 'end') self.parent.icursor('end') # return 'break' because, for some reason, Control-a (little 'a') # doesn't work otherwise. There's some natural binding that # Tkinter entry widgets want to do that send the cursor to Home # and deselects. return 'break' def paste_string_state(self): """Returns true if a string is in the clipboard""" try: # this assignment will raise an exception if the data # in the clipboard isn't a string (such as a picture). # in which case we want to know about it so that the Paste # option can be appropriately set normal or disabled. clipboard = self.parent.selection_get(selection='CLIPBOARD') except: return False return True class RightClickMenuForScrolledText(object): """Simple widget to add basic right click menus to entry widgets.""" def __init__(self, parent): self.parent = parent # bind Control-A to select_all() to the widget. All other # accelerators seem to work fine without binding such as # Ctrl-V, Ctrl-X, Ctrl-C etc. Ctrl-A was the only one I had # issue with. self.parent.bind("<Control-a>", lambda e: self._select_all(), add='+') self.parent.bind("<Control-A>", lambda e: self._select_all(), add='+') def __call__(self, event): # if the entry widget is disabled do nothing. if self.parent.cget('state') == tk.DISABLED: return # grab focus of the entry widget. this way you can see # the cursor and any marking selections self.parent.focus_force() self.build_menu(event) def build_menu(self, event): """build menu""" menu = tk.Menu(self.parent, tearoff=0) # check to see if there is any marked text in the entry widget. # if not then Cut and Copy are disabled. # if not self.parent.selection_get(): # menu.add_command(label="Cut", state=tk.DISABLED) # menu.add_command(label="Copy", state=tk.DISABLED) # else: # use Tkinter's virtual events for brevity. These could # be hardcoded with our own functions to immitate the same # actions but there's no point except as a novice exercise # (which I recommend if you're a novice). menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_CUT"), command=self._cut) menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_COPY"), command=self._copy) # if there's string data in the clipboard then make the normal # Paste command. otherwise disable it. if self._paste_string_state(): menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_PASTE"), command=self._paste_if_string_in_clipboard) else: menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_PASTE"), state='disable') # again, if there's no marked text then the Delete option is disabled. menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_DELETE"), command=self._delete) # make things pretty with a horizontal separator menu.add_separator() # I don't know of if there's a virtual event for select all though # I did look in vain for documentation on -any- of Tkinter's # virtual events. Regardless, the method itself is trivial. menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_SELECT"), command=self._select_all) menu.add_command( label=TEXT_DICT.get("buttons").get("RIGHT_MENU_CLEAR"), command=self._clear_all) menu.post(event.x_root, event.y_root) def _cut(self): self.parent.event_generate("<<Cut>>") def _copy(self): self.parent.event_generate("<<Copy>>") def _delete(self): self.parent.event_generate("<<Clear>>") def _paste_if_string_in_clipboard(self): self.parent.event_generate("<<Paste>>") def _select_all(self, ): """select all""" self.parent.tag_add('sel', "1.0", "end-1c") self.parent.mark_set('insert', "1.0") self.parent.see('insert') return 'break' def _paste_string_state(self): """Returns true if a string is in the clipboard""" try: # this assignment will raise an exception if the data # in the clipboard isn't a string (such as a picture). # in which case we want to know about it so that the Paste # option can be appropriately set normal or disabled. clipboard = self.parent.selection_get(selection='CLIPBOARD') except: return False return True def _clear_all(self): """Clear all""" isok = tkMessageBox.askokcancel( TEXT_DICT.get("buttons").get("RIGHT_MENU_CLEAR"), TEXT_DICT.get("notice").get("CLEAR_ALL_NOTICE"), parent=self.parent, default='ok') if isok: self.parent.delete('1.0', 'end') class TextEmit(object): """Redirect stdout and stderr to tk widgets.""" def __init__(self, widget, tag='stdout'): """Initialize widget which stdout and stderr were redirected to.""" self.widget = widget self.tag = tag def write(self, out_str): """Proceed Redirection.""" self.widget.configure(state='normal') self.widget.insert('end', out_str, (self.tag,)) self.widget.tag_configure('stderr', foreground='red', background='yellow') self.widget.configure(state='disabled') self.widget.see('end') class App(tk.Frame): """The main class for GUI application. [Example] >>> app = App() >>> app.mainloop() """ def __init__(self, master=None): tk.Frame.__init__(self, master) # Setting title. self.master.title(GUI_TITLE) # Setting initial window size. self.master.geometry(INIT_WINDOW_SIZE) self.final_tree = '' self.file_path_history_list = [] self.combo_line_count = 0 # GUI creating self.set_style() self.create_widgets() self.configure_grid() self.row_and_column_configure() self.create_right_menu() self.bind_func() self.display_info() self.create_menu_bar() def set_style(self): """Set custom style for widget.""" s = ttk.Style() # Configure button style s.configure('TButton', padding=5) s.configure( 'execute.TButton', foreground='red', ) s.configure( 'newline.TButton', padding=6), s.configure( 'clear.TButton', foreground='#2AA198', ) # Configure Combobox style s.configure('TCombobox', padding=6) s.configure('config.TCombobox', ) # Configure Title Frame s.configure( 'title.TLabel', padding=10, font=('helvetica', 11, 'bold'), ) s.configure( 'config.TLabel', padding=1, font=('arial', 9), ) def create_widgets(self): """Create widgets in the main window. There are four main panes: 1. tree_pane 2. config_pane 3. out_tree_pane 4. log_pane """ # +-------------------+-------------------+ # | | | # | | | # | tree_pane | config_pane | # | | | # | | | # +-------------------+-------------------+ # | | | # | | | # | out_tree_pane | log_pane | # | | | # | | | # +-------------------+-------------------+ self.tree_pane = ttk.Frame(self.master, padding=(5)) self.config_pane = ttk.Frame(self.master, padding=(5)) self.out_tree_pane = ttk.Frame(self.master, padding=(5)) self.log_pane = ttk.Frame(self.master, padding=(5)) # +-------------------+-------------------+ # | | | # | 1 | | # | tree_pane | | # | | | # | | | # +-------------------+-------------------+ # | | | # | | | # | | | # | | | # | | | # +-------------------+-------------------+ self.choose_tree_label = ttk.Label( self.tree_pane, text=TEXT_DICT.get('labels').get('TREE_PANE_LABEL'), style='title.TLabel', ) self.open_tree_file_button = ttk.Button( self.tree_pane, text=TEXT_DICT.get('buttons').get('OPEN_TREE_BUTTON'), ) self.clear_tree_input = ttk.Button( self.tree_pane, text=TEXT_DICT.get('buttons').get('CLEAR_BUTTON'), style='clear.TButton', ) self.tree_name = tk.StringVar() self.choose_tree_box = ttk.Combobox(self.tree_pane, textvariable=self.tree_name) self.load_history_button = ttk.Button( self.tree_pane, text=TEXT_DICT.get('buttons').get('LOAD_HISTORY_BUTTON'), ) self.tree_paste_area = st.ScrolledText( self.tree_pane) # +-------------------+-------------------+ # | | | # | | 2 | # | | config_pane | # | | | # | | | # +-------------------+-------------------+ # | | | # | | | # | | | # | | | # | | | # +-------------------+-------------------+ self.config_label = ttk.Label( self.config_pane, text=TEXT_DICT.get('labels').get('CONFIG_PANE_LABEL'), style='title.TLabel' ) self.execute_button = ttk.Button( self.config_pane, text=TEXT_DICT.get('buttons').get('EXECUTE_ALL_BUTTON'), style='execute.TButton', ) self.clear_config_area_button = ttk.Button( self.config_pane, text=TEXT_DICT.get('buttons').get('CLEAR_BUTTON'), style='clear.TButton', ) self.read_config_file_button = ttk.Button( self.config_pane, text=TEXT_DICT.get('buttons').get('READ_CONFIG_BUTTON'), ) self.save_config_to_file_button = ttk.Button( self.config_pane, text=TEXT_DICT.get('buttons').get('SAVE_CONFIG_BUTTON'), ) self.name_a_label = ttk.Label( self.config_pane, text=TEXT_DICT.get('labels').get('NAME_A_LABEL'), style='config.TLabel') self.name_b_label = ttk.Label( self.config_pane, text=TEXT_DICT.get('labels').get('NAME_B_LABEL'), style='config.TLabel') self.info_label = ttk.Label( self.config_pane, text=TEXT_DICT.get('labels').get('INFO_LABEL'), style='config.TLabel') self.add_newline_button = ttk.Button( self.config_pane, text=TEXT_DICT.get('buttons').get('ADD_NEW_BUTTON'), style='newline.TButton', ) self.name_a_combobox = ttk.Combobox( self.config_pane, style='config.TCombobox') self.name_b_combobox = ttk.Combobox( self.config_pane, style='config.TCombobox') self.info_combobox = ttk.Combobox( self.config_pane, style='config.TCombobox') self.config_lines_area = st.ScrolledText( self.config_pane, ) # +-------------------+-------------------+ # | | | # | | | # | | | # | | | # | | | # +-------------------+-------------------+ # | | | # | 3 | | # | out_tree_pane | | # | | | # | | | # +-------------------+-------------------+ self.out_tree_label = ttk.Label( self.out_tree_pane, text=TEXT_DICT.get('labels').get('OUTPUT_LABEL'), style='title.TLabel', ) self.view_as_ascii_button = ttk.Button( self.out_tree_pane, text=TEXT_DICT.get('buttons').get('VIEW_ASCII_BUTTON'), ) self.save_current_dir_button = ttk.Button( self.out_tree_pane, text=TEXT_DICT.get('buttons').get('QUICK_SAVE_BUTTON'), ) self.save_as_button = ttk.Button( self.out_tree_pane, text=TEXT_DICT.get('buttons').get('SAVE_TREE_AS_BUTTON'), ) # Clear out tree button self.clear_out_tree_button = ttk.Button( self.out_tree_pane, text=TEXT_DICT.get('buttons').get('CLEAR_BUTTON'), style='clear.TButton', ) self.clear_out_tree_button.grid(row=0, column=4, sticky='we') # Out Tree area self.out_tree_area = st.ScrolledText(self.out_tree_pane, bg='#FAFAFA') # +-------------------+-------------------+ # | | | # | | | # | | | # | | | # | | | # +-------------------+-------------------+ # | | | # | | 4 | # | | log_pane | # | | | # | | | # +-------------------+-------------------+ self.log_label = ttk.Label( self.log_pane, text=TEXT_DICT.get('labels').get('LOG_PANE'), style='title.TLabel') self.cali_display_width_lable = ttk.Label( self.log_pane, text=TEXT_DICT.get('labels').get('DISPLAY_WIDTH_LABEL'), style='config.TLabel' ) self.cali_display_width_combobox = ttk.Combobox( self.log_pane, style='config.TCombobox') self.cali_display_width_combobox.set(INSERT_POSITION_HALF_SIZE) # Save log button self.save_log_button = ttk.Button( self.log_pane, text=TEXT_DICT.get('buttons').get('SAVE_LOG_AS_BUTTON'), ) # Clear out tree button self.clear_log_button = ttk.Button( self.log_pane, text=TEXT_DICT.get('buttons').get('CLEAR_BUTTON'), style='clear.TButton', ) self.log_area = st.ScrolledText( self.log_pane, fg=LOG_AREA_FOREGROUND, bg=LOG_AREA_BACKGROUND, state='disabled', ) def configure_grid(self): self.master.grid() # +-------------------+-------------------+ # | | | # | | | # | tree_pane | config_pane | # | | | # | | | # +-------------------+-------------------+ # | | | # | | | # | out_tree_pane | log_pane | # | | | # | | | # +-------------------+-------------------+ self.tree_pane.grid(row=0, column=0, sticky='wens') self.config_pane.grid(row=0, column=1, sticky='wens') self.out_tree_pane.grid(row=1, column=0, sticky='wens') self.log_pane.grid(row=1, column=1, sticky='wens') # tree_pane self.choose_tree_label.grid(row=0, column=0, sticky='w') self.open_tree_file_button.grid(row=0, column=1, sticky='we') self.clear_tree_input.grid(row=0, column=2, sticky='we') self.choose_tree_box.grid(row=1, column=0, columnspan=2, sticky='we') self.load_history_button.grid(row=1, column=2, sticky='e') self.tree_paste_area.grid( row=2, column=0, columnspan=3, sticky='wens') # config_pane self.config_label.grid(row=0, column=0, sticky='w') self.execute_button.grid(row=0, column=2, sticky='we') self.clear_config_area_button.grid(row=0, column=3, sticky='we') self.read_config_file_button.grid(row=1, column=2, sticky='we') self.save_config_to_file_button.grid(row=1, column=3, sticky='we') self.name_a_label.grid(row=2, column=1, sticky='ws') self.name_b_label.grid(row=2, column=2, sticky='ws') self.info_label.grid(row=2, column=3, sticky='ws') self.add_newline_button.grid(row=3, column=0, sticky='we') self.name_a_combobox.grid(row=3, column=1, sticky='we') self.name_b_combobox.grid(row=3, column=2, sticky='we') self.info_combobox.grid(row=3, column=3, sticky='we') self.config_lines_area.grid( row=4, column=0, columnspan=4, sticky='wens') # out_tree_pane self.out_tree_label.grid(row=0, column=0, sticky='w') self.view_as_ascii_button.grid(row=0, column=1, sticky='we') self.save_current_dir_button.grid(row=0, column=2, sticky='we') self.save_as_button.grid(row=0, column=3, sticky='we') self.out_tree_area.grid( row=1, column=0, columnspan=5, sticky='wens') # log_pane self.log_label.grid(row=0, column=0, sticky='w') self.save_log_button.grid(row=0, column=1, sticky='we') self.clear_log_button.grid(row=0, column=2, sticky='we') self.cali_display_width_lable.grid(row=1, column=1, sticky='we') self.cali_display_width_combobox.grid(row=1, column=2, sticky='we') self.log_area.grid(row=2, column=0, columnspan=3, sticky='wens') def row_and_column_configure(self): # +-------------------+-------------------+ # | | | # | | | # | row: 0, column: 0 | row: 0, column: 1 | # | | | # | | | # +-------------------+-------------------+ # | | | # | | | # | row: 1, column: 0 | row: 1, column: 1 | # | | | # | | | # +-------------------+-------------------+ # Configure row and column self.master.rowconfigure(0, weight=1, uniform='fred') self.master.rowconfigure(1, weight=1, uniform='fred') self.master.columnconfigure(0, weight=1, uniform='fred') self.master.columnconfigure(1, weight=1, uniform='fred') # Orig Tree, Left Up self.tree_pane.rowconfigure(0, weight=0) self.tree_pane.rowconfigure(1, weight=0) self.tree_pane.rowconfigure(2, weight=1) self.tree_pane.columnconfigure(0, weight=1) self.tree_pane.columnconfigure(1, weight=0) self.tree_pane.columnconfigure(2, weight=0) # Config label, Right Up self.config_pane.rowconfigure(0, weight=0) self.config_pane.rowconfigure(1, weight=0) self.config_pane.rowconfigure(2, weight=0) self.config_pane.rowconfigure(3, weight=0) self.config_pane.rowconfigure(4, weight=1) self.config_pane.columnconfigure(0, weight=0) self.config_pane.columnconfigure(1, weight=1) self.config_pane.columnconfigure(2, weight=1) self.config_pane.columnconfigure(3, weight=1) # Out tree, Left Down self.out_tree_pane.rowconfigure(0, weight=0) self.out_tree_pane.rowconfigure(1, weight=1) self.out_tree_pane.columnconfigure(0, weight=1) self.out_tree_pane.columnconfigure(1, weight=0) self.out_tree_pane.columnconfigure(2, weight=0) self.out_tree_pane.columnconfigure(3, weight=0) # Log pane, Right Down self.log_pane.rowconfigure(0, weight=0) self.log_pane.rowconfigure(1, weight=0) self.log_pane.rowconfigure(2, weight=1) self.log_pane.columnconfigure(0, weight=1) self.log_pane.columnconfigure(1, weight=0) self.log_pane.columnconfigure(2, weight=0) def create_right_menu(self): def _bind_right_menu_for_all_platforms(widget, right_menu_obj): # Bind right click menu to all platforms if CURRENT_PLATFORM in (MAC_PLATFORM,): widget.bind('<Button-2>', right_menu_obj) else: widget.bind('<Button-3>', right_menu_obj) # Right click menu for choose tree combobox right_menu_tree_choose = RightClickMenu(self.choose_tree_box) _bind_right_menu_for_all_platforms(self.choose_tree_box, right_menu_tree_choose) # Right Click menu for tree input area right_menu_input = RightClickMenuForScrolledText(self.tree_paste_area) _bind_right_menu_for_all_platforms(self.tree_paste_area, right_menu_input) # Right click menu for three input combobox in config_pane right_menu_name_a = RightClickMenu(self.name_a_combobox) _bind_right_menu_for_all_platforms(self.name_a_combobox, right_menu_name_a) right_menu_name_b = RightClickMenu(self.name_b_combobox) _bind_right_menu_for_all_platforms(self.name_b_combobox, right_menu_name_b) right_menu_info_combobox = RightClickMenu(self.info_combobox) _bind_right_menu_for_all_platforms(self.info_combobox, right_menu_info_combobox) # Right click menu for config input area right_menu_config = RightClickMenuForScrolledText( self.config_lines_area) _bind_right_menu_for_all_platforms(self.config_lines_area, right_menu_config) # Right click menu for output area right_menu_out = RightClickMenuForScrolledText(self.out_tree_area) _bind_right_menu_for_all_platforms(self.out_tree_area, right_menu_out) def bind_func(self): # input_pane self.open_tree_file_button['command'] = self._ask_open_file self.clear_tree_input['command'] = \ lambda: self.tree_paste_area.delete('1.0', 'end') self.load_history_button['command'] = self._load_history_file # config_pane self.clear_config_area_button['command'] = lambda: \ self.config_lines_area.delete('1.0', 'end') self.read_config_file_button['command'] = self._read_config_from_file self.save_config_to_file_button['command'] = self._save_config_to_file self.add_newline_button['command'] = self._set_value_to_textarea self.execute_button['command'] = self._main_work # output_pane self.view_as_ascii_button['command'] = self._view_as_ascii_command self.save_current_dir_button['command'] = \ self._save_new_tree_to_current_dir self.save_as_button['command'] = self._ask_save_out_as_file self.clear_out_tree_button['command'] = \ lambda: self.out_tree_area.delete('1.0', 'end') # log_pane self.save_log_button['command'] = self._ask_save_log_as_file self.clear_log_button['command'] = self._clear_log def create_menu_bar(self): """Create Menu Bar for NodeFinderGUI""" menu_bar = tk.Menu(self.master) # File Menu file_menu = tk.Menu(menu_bar, tearoff=0) file_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_OPEN'), command=self._ask_open_file) file_menu.add_separator() file_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_SAVE_TO'), command=self._ask_save_out_as_file) file_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_SAVE_LOG'), command=self._ask_save_log_as_file) file_menu.add_separator() file_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_EXIT'), command=self.quit) menu_bar.add_cascade( label=TEXT_DICT.get('menubar').get('MENUBAR_FILE_CASCADE'), menu=file_menu) # Configure Menu configs_menu = tk.Menu(menu_bar, tearoff=0) configs_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_OPEN_CONFIG'), command=self._read_config_from_file) configs_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_SAVE_CONFIG'), command=self._save_config_to_file) menu_bar.add_cascade( label=TEXT_DICT.get('menubar').get('MENUBAR_CONFIG_CASCADE'), menu=configs_menu) # Edit Menu edit_menu = tk.Menu(menu_bar, tearoff=0) edit_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_CUT'), command=self._cut) edit_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_COPY'), command=self._copy) # try: # edit_menu.add_command(label="Paste", command=self._paste) # except Exception: # pass if self._paste_string_state(): edit_menu.add_command( label=TEXT_DICT.get('menubar').get("MENUBAR_PASTE"), command=self._paste) else: edit_menu.add_command( label=TEXT_DICT.get('menubar').get("MENUBAR_PASTE"), command=lambda: print(TEXT_DICT.get( 'notice').get('NO_STR_IN_CLIPBOARD'))) edit_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_DELETE'), command=self._delete) menu_bar.add_cascade( label=TEXT_DICT.get('menubar').get('MENUBAR_EDIT_CASCADE'), menu=edit_menu) # Help Menu help_menu = tk.Menu(menu_bar, tearoff=0) help_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_DOC'), command=self.display_documentation) help_menu.add_command( label=TEXT_DICT.get('menubar').get('MENUBAR_ABOUT'), command=self.display_about) menu_bar.add_cascade( label=TEXT_DICT.get('menubar').get('MENUBAR_HELP_CASCADE'), menu=help_menu) self.master.config(menu=menu_bar) def display_documentation(self): """Display documentation for menu bar about button.""" print(LONG_BAR) print(DOCUMENTATION) print(LONG_BAR) def display_about(self): """Display about information for menu bar about button.""" print(LONG_BAR) print(ABOUT) print(LONG_BAR) def display_info(self): sys.stdout = TextEmit(self.log_area, 'stdout') sys.stderr = TextEmit(self.log_area, 'stderr') print(LONG_BAR) print(FORMAT_STR_GUI_TITLE % (GUI_TITLE, __version__)) print(time.strftime(FORMAT_STR_TIME, time.localtime())) print(LONG_BAR) print(HELP) def _copy(self): # self.master.clipboard_clear() # self.master.clipboard_append(self.master.focus_get().selection_get()) self.master.focus_get().event_generate("<<Copy>>") def _cut(self): # self.master.clipboard_clear() # self.master.clipboard_append(self.master.focus_get().selection_get()) # # self.master.focus_get().selection_clear() self.master.focus_get().event_generate("<<Cut>>") def _paste(self): # print(self.master.selection_get(selection='CLIPBOARD')) self.master.focus_get().event_generate("<<Paste>>") def _delete(self): self.master.focus_get().event_generate("<<Clear>>") def _paste_string_state(self): """Returns true if a string is in the clipboard""" try: # this assignment will raise an exception if the data # in the clipboard isn't a string (such as a picture). # in which case we want to know about it so that the Paste # option can be appropriately set normal or disabled. clipboard = self.master.selection_get(selection='CLIPBOARD') except: return False return True def _ask_open_file(self): """Dialog to open file.""" file_opt = {} c = tkFileDialog.askopenfile(mode='r', **file_opt) try: orig_tree_str = c.read() self.tree_paste_area.delete('1.0', 'end') self.tree_paste_area.insert('end', orig_tree_str) abs_path = c.name base_name = os.path.basename(abs_path) print('[ INFO | %s ] Open tree file: %s' % (time_now(), base_name)) # Add to history (Feature Not Implemented) self.file_path_history_list.insert(0, abs_path) self.choose_tree_box['values'] = self.file_path_history_list self.choose_tree_box.current('0') except AttributeError: print('[ INFO | %s ] No file choosed' % time_now()) def _load_history_file(self): """Load file from history.""" file_path = self.choose_tree_box.get() if not file_path: sys.stderr.write('[ ERROR | %s ] History file bar is blank\n' % time_now()) elif not os.path.isfile(file_path): sys.stderr.write('[ ERROR | %s ] No such file\n' % time_now()) else: with open(file_path, 'r') as f: content = f.read() self.tree_paste_area.delete('1.0', 'end') self.tree_paste_area.insert('end', content) print('[ INFO | %s ] Load file' % time_now()) def _read_config_from_file(self): """Read calibration config from file""" file_opt = {} c = tkFileDialog.askopenfile(mode='r', **file_opt) if c is None: # No file selected return config_content = c.read() self.config_lines_area.delete('1.0', 'end') self.config_lines_area.insert('end', config_content) abs_path = c.name base_name = os.path.basename(abs_path) print('[ INFO | %s ] Read from config file: %s' % (time_now(), base_name)) def _save_config_to_file(self): """Save current calibration config content to file""" f = tkFileDialog.asksaveasfile(mode='w', defaultextension=".txt") # asksaveasfile return `None` if dialog closed with "cancel". if f is None: return text_to_save = str(self.config_lines_area.get('1.0', 'end-1c')) f.write(text_to_save) f.close() def _set_value_to_textarea(self): """Value to textarea.""" name_a, name_b, info = self.name_a_combobox.get(), \ self.name_b_combobox.get(), self.info_combobox.get() config_list = filter(lambda x: x != '', [name_a, name_b, info]) if len(config_list) < 2 or not info: sys.stderr.write('[ ERROR | %s ]\n[Usage]\n' % time_now()) sys.stderr.write( ' Calibration: name_a, name_b, cali_info\n') sys.stderr.write( ' Branch Label: name_a, branch_label\n') sys.stderr.write( ' Clade Label: name_a, name_b, clade_label\n') print('') else: one_line = ', '.join(config_list) self.config_lines_area.insert('end', one_line + '\n') print('[ INFO - %s ] Added one configure line (%s)' % (time_now(), one_line)) def _view_as_ascii_command(self): """View tree using ascii tree program.""" new_tree_str = self.out_tree_area.get('1.0', 'end-1c') if not new_tree_str: sys.stderr.write('[ ERROR | %s] No content in out tree ' 'area to view' % time_now()) tkMessageBox.showerror( 'ValueError', 'No content in Tree Output area to view.') try: with open('tmp_file_for_ascii_view.nwk', 'w') as f: f.write(new_tree_str) p = Popen( [ 'python', 'tree_ascii_view.pyw', 'tmp_file_for_ascii_view.nwk' ], stdout=PIPE, stderr=PIPE) print(p.communicate()[0]) if p.communicate()[1]: sys.stderr.write(p.communicate()[1]) except IOError as e: tkMessageBox.showerror( title='File Error', message='Cannot write temporary file to disk.\n%s' % e) # Quick Save button def _save_new_tree_to_current_dir(self): """Quick save Newick tree to current folder.""" new_tree_content = self.out_tree_area.get('1.0', 'end-1c') new_tree_name = 'New_tree.nwk' with open(new_tree_name, 'w') as f: f.write(new_tree_content) print('[ INFO | %s ] Quick save: (%s)' % ( time_now(), new_tree_name)) # Save as button for outcome def _ask_save_out_as_file(self): """Dialog to save as file.""" f = tkFileDialog.asksaveasfile(mode='w', defaultextension=".txt") # asksaveasfile return `None` if dialog closed with "cancel". if f is None: return text_to_save = str(self.out_tree_area.get('1.0', 'end-1c')) f.write(text_to_save) f.close() # Save as button for log def _ask_save_log_as_file(self): """Dialog to save as file.""" f = tkFileDialog.asksaveasfile(mode='w', defaultextension=".txt") # asksaveasfile return `None` if dialog closed with "cancel". if f is None: return text_to_save = str(self.log_area.get('1.0', 'end-1c')) f.write(text_to_save) f.close() def _clear_log(self): """Clear all contents in log widget area.""" self.log_area.configure(state='normal') self.log_area.delete('1.0', 'end') self.log_area.configure(state='disabled') def _apply_values_from_widgets(self): global INSERT_POSITION_HALF_SIZE insert_position_half_size_now = int(float( self.cali_display_width_combobox.get())) if insert_position_half_size_now != INSERT_POSITION_HALF_SIZE: INSERT_POSITION_HALF_SIZE = insert_position_half_size_now def _main_work(self): """Do main job.""" self._apply_values_from_widgets() tree_str = get_tree_str(self.tree_paste_area.get('1.0', 'end-1c')) calibration_list = get_cali_list( self.config_lines_area.get('1.0', 'end-1c')) if not calibration_list: sys.stderr.write("No valid config lines or error in config lines!") else: self.final_tree = multi_calibration(tree_str, calibration_list) if self.final_tree: self.out_tree_area.delete('1.0', 'end') self.out_tree_area.insert('end', self.final_tree) def hello(self): """Simple hello function for testing use.""" print('Hello NodeFinderGUI!') def clean_elements(orig_list): """Strip each element in list and return a new list. [Params] orig_list: Elements in original list is not clean, may have blanks or newlines. [Return] clean_list: Elements in clean list is striped and clean. [Example] >>> clean_elements(['a ', '\tb\t', 'c; ']) ['a', 'b', 'c'] """ return [_.strip().strip(';') for _ in orig_list] def get_clean_tree_str(tree_str): """Remove all blanks and return a very clean tree string. >>> get_clean_tree_str('((a ,((b, c), (d, e))), (f, g));') '((a,((b,c),(d,e))),(f,g));' """ return tree_str.replace(' ', '').replace('\n', '').replace('\t', '') def get_right_index_of_name(clean_tree_str, one_name): """Get the right index of givin name. # 111111111122222222 # 0123456789012345678901234567 # | >>> get_right_index_of_name('((a,((b,c),(ddd,e))),(f,g));', 'ddd') 15 """ left_index_of_name = clean_tree_str.find(one_name) while clean_tree_str[left_index_of_name] not in \ NONE_TREE_NAME_SYMBOL_SET: left_index_of_name += 1 return left_index_of_name def get_insertion_list(clean_tree_str, name): """Get insertion list """ insertion_list = [] current_index = clean_tree_str.find(name) stack = [] str_len = len(clean_tree_str) while current_index < str_len: if clean_tree_str[current_index] == '(': stack.append('(') elif clean_tree_str[current_index] == ')': if not stack: insertion_list.append(current_index + 1) else: stack.pop() current_index += 1 return insertion_list def get_index_of_tmrca(clean_tree_str, name_a, name_b): """Get index of the most recent common ancestor""" insertion_list_a = get_insertion_list(clean_tree_str, name_a) insertion_list_b = get_insertion_list(clean_tree_str, name_b) # print(insertion_list_a) # print(insertion_list_b) insertion_list_a, insertion_list_b = insertion_list_a[::-1], \ insertion_list_b[::-1] shorter_list = insertion_list_a if len(insertion_list_a) < \ len(insertion_list_b) else insertion_list_b longer_list = insertion_list_a if shorter_list == insertion_list_b else \ insertion_list_b # print('[Shorter List]: ', shorter_list) # print('[Longer List]: ', longer_list) for i, each_in_shorter_list in enumerate(shorter_list): if i == len(shorter_list) - 1: cali_point = each_in_shorter_list if shorter_list[i] != longer_list[i]: cali_point = shorter_list[i - 1] break tree_len = len(clean_tree_str) print('[Common]: %s\n' % cali_point) if cali_point < INSERT_POSITION_HALF_SIZE <= tree_len - cali_point: print('[Insert]: %s%s' % ( ' ' * (INSERT_POSITION_HALF_SIZE - cali_point), clean_tree_str[:cali_point + INSERT_POSITION_HALF_SIZE]) ) elif cali_point >= INSERT_POSITION_HALF_SIZE > tree_len - cali_point: print('[Insert]: %s' % (clean_tree_str[cali_point - INSERT_POSITION_HALF_SIZE:cali_point + (tree_len - cali_point)]) ) elif cali_point < INSERT_POSITION_HALF_SIZE and \ tree_len - cali_point < INSERT_POSITION_HALF_SIZE: print('[Insert]: %s%s' % ( ' ' * (INSERT_POSITION_HALF_SIZE - cali_point), clean_tree_str[:cali_point + (tree_len - cali_point)]) ) else: print('[Insert]: %s' % (clean_tree_str[cali_point - INSERT_POSITION_HALF_SIZE:cali_point + INSERT_POSITION_HALF_SIZE]) ) print('[Insert]: %s ->||<-' % (' ' * (INSERT_POSITION_HALF_SIZE-5))) print('[Insert]: %sInsert Here' % (' ' * (INSERT_POSITION_HALF_SIZE-5))) return cali_point def single_calibration(tree_str, name_a, name_b, cali_info): """Do single calibration. If calibration exists, replace it.""" clean_tree_str = get_clean_tree_str(tree_str) cali_point = get_index_of_tmrca(clean_tree_str, name_a, name_b) # Check if there are duplicate calibration current_info = '%s, %s, %s' % (name_a, name_b, cali_info) if cali_point not in global_insertion_list_cache: global_insertion_list_cache[cali_point] = current_info else: print('\n[Warning] Duplicate calibration: [ !!! ]') print('[Exists]: %s\n' '[ Now ]: %s\n' % (global_insertion_list_cache[cali_point], current_info)) # No calibration before if clean_tree_str[cali_point] in NO_CALI_EXISTS_SYMBOL_SET: left_part, right_part = clean_tree_str[:cali_point], \ clean_tree_str[cali_point:] clean_str_with_cali = left_part + cali_info + right_part # There was calibration there # '>': >0.05<0.07 # '<': <0.38 # '@': @0.56 # '0': 0.5 # '1': 1 # "'": '>0.05<0.07' # '"': ">0.05<0.07" # '$': $1 # ':': :0.12345 elif clean_tree_str[cali_point] in WITH_CALI_EXISTS_SYMBOL_SET: # ((a,((b,c),(d,e)))>0.3<0.5,(f,g)); # left_part = '((a,((b,c),(d,e)))' # right_part = '>0.3<0.5,(f,g));' # re will find '>0.3<0.5' part re_find_left_cali = re.compile('^[^,);]+') left_part, right_part = clean_tree_str[:cali_point], \ clean_tree_str[cali_point:] left_cali = re_find_left_cali.findall(right_part)[0] print('[Calibration Exists]: ', left_cali, ' [- Old]') print('[Calibration Replaced By]: ', cali_info, ' [+ New]') # '>0.3<0.5,(f,g));'.lstrip('>0.3<0.5') will be ',(f,g));' final_right_part = right_part.lstrip(left_cali) clean_str_with_cali = left_part + cali_info + final_right_part else: raise ValueError('Unknown: ' + clean_tree_str[cali_point]) return clean_str_with_cali def add_single_branch_label(tree_str, name_a, branch_label): """Add single label right after one name. >>> add_single_branch_label('((a ,((b, c), (d, e))), (f, g));', c, '#1') '((a ,((b, c #1 ), (d, e))), (f, g));' """ clean_tree_str = get_clean_tree_str(tree_str) insert_point = get_right_index_of_name(clean_tree_str, name_a) tree_len = len(clean_tree_str) print('[Common]: %s\n' % insert_point) if insert_point < INSERT_POSITION_HALF_SIZE <= tree_len - insert_point: print('[Insert]: %s%s' % ( ' ' * (INSERT_POSITION_HALF_SIZE - insert_point), clean_tree_str[:insert_point + INSERT_POSITION_HALF_SIZE]) ) elif insert_point >= INSERT_POSITION_HALF_SIZE > tree_len - insert_point: print('[Insert]: %s' % ( clean_tree_str[ insert_point - INSERT_POSITION_HALF_SIZE:insert_point + (tree_len - insert_point)]) ) elif insert_point < INSERT_POSITION_HALF_SIZE and \ tree_len - insert_point < INSERT_POSITION_HALF_SIZE: print('[Insert]: %s%s' % (' ' * (INSERT_POSITION_HALF_SIZE - insert_point), clean_tree_str[:insert_point + (tree_len - insert_point)]) ) else: print('[Insert]: %s' % ( clean_tree_str[ insert_point - INSERT_POSITION_HALF_SIZE:insert_point + INSERT_POSITION_HALF_SIZE]) ) print('[Insert]: %s ->||<-' % (' ' * (INSERT_POSITION_HALF_SIZE-5))) print('[Insert]: %sInsert Here' % (' ' * (INSERT_POSITION_HALF_SIZE-5))) # Check is there was something there # Nothing there before if clean_tree_str[insert_point] in NO_LABEL_EXISTS_SYMBOL_SET: left_part, right_part = clean_tree_str[:insert_point], \ clean_tree_str[insert_point:] clean_str_with_cali = left_part + ' %s ' % branch_label + right_part # There was calibration there # '>': >0.05<0.07 # '<': <0.38 # '@': @0.56 # '0': 0.5 # '1': 1 # "'": '>0.05<0.07' # '"': ">0.05<0.07" # '$': $1 # ':': :0.12345 elif clean_tree_str[insert_point] in WITH_LABEL_EXISTS_SYMBOL_SET: # ((a,((b,c),(d,e)))>0.3<0.5,(f,g)); # left_part = '((a,((b,c),(d,e)))' # right_part = '>0.3<0.5,(f,g));' # re will find '>0.3<0.5' part re_find_left_cali = re.compile('^[^,);]+') left_part, right_part = (clean_tree_str[:insert_point], clean_tree_str[insert_point:]) left_cali = re_find_left_cali.findall(right_part)[0] print('[Label Exists]: ' + left_cali + ' [- Old]') print('[Label Replaced By]: ' + branch_label + ' [+ New]') # '>0.3<0.5,(f,g));'.lstrip('>0.3<0.5') will be ',(f,g));' final_right_part = right_part.lstrip(left_cali) clean_str_with_cali = (left_part + ' %s ' % branch_label + final_right_part) else: raise ValueError('[Error] [Unknown Symbol]: ' + clean_tree_str[insert_point]) return clean_str_with_cali def multi_calibration(tree_str, cali_tuple_list): """Do calibration for multiple calibration requests.""" global global_insertion_list_cache global_insertion_list_cache = {} species_names_from_tree_str = get_species_names_from_tree_str(tree_str) print('\n\n====================================================') print(' [ New Job: %s]' % time_now()) print('====================================================') # Print valid calibration information print('\n[Valid Calibrations]\n') for i, each_cali_tuple in enumerate(cali_tuple_list): print('%4d | %s' % (i + 1, ', '.join(each_cali_tuple))) # Check whether all species names from config lines are in Newick tree if check_all_names_in_newick_tree(tree_str, cali_tuple_list): # Do multiple calibrations for i, each_cali_tuple in enumerate(cali_tuple_list): if len(each_cali_tuple) == 3: name_a, name_b, cali_or_clade_info = each_cali_tuple print('\n') print(THIN_BAR) print('[%d]: %s' % (i + 1, ', '.join(each_cali_tuple))) print(THIN_BAR) print('[Name A]: ', name_a) print('[Name B]: ', name_b) print('[ Info ]: ', cali_or_clade_info) if cali_or_clade_info[0] not in \ WARNING_CALI_OR_LABEL_INFO_SYMBOL_SET: print('\n[Warning]: Is this valid symbel? %s [ !!! ]\n' % cali_or_clade_info) tree_str = single_calibration(tree_str, name_a, name_b, cali_or_clade_info) elif len(each_cali_tuple) == 2: name_a, branch_label = each_cali_tuple print('\n') print(THIN_BAR) print('[%d]: %s' % (i + 1, ', '.join(each_cali_tuple))) print(THIN_BAR) print('[ Name ]: ', name_a) print('[ Info ]: ', branch_label) if branch_label[0] not in WARNING_BRANCH_LABEL_SYMBOL_SET: print('\n[Warning]: Is this valid symbel? %s [ !!! ]\n' % branch_label) tree_str = add_single_branch_label(tree_str, name_a, branch_label) final_tree = tree_str.replace(',', ', ') return final_tree else: sys.stderr.write("Please check config lines!\n") return None def get_cali_list(raw_cali_content): """Get calibration list.""" tmp_cali_list = [] lines = [_.strip() for _ in raw_cali_content.split('\n') if _.strip()] for i, line in enumerate(lines): line = line.strip() if line[0] in {'#', '//'}: continue elements = clean_elements(line.split(',')) if len(elements) not in [2, 3]: sys.stderr.write('Invalid config line (Line: %d): %s\n' % (i + 1, line)) return [] else: tmp_cali_list.append(elements) return tmp_cali_list def get_tree_str(raw_tree_content): """Read tree content, parse, and return tree string""" tmp_tree_str = '' tree_start_flag = False lines = raw_tree_content.split('\n') for line in lines: line = line.strip() if line.startswith('('): tree_start_flag = True if not tree_start_flag: continue if line.startswith('//') or line.startswith('#'): break else: tmp_tree_str += line return tmp_tree_str def get_species_names_from_tree_str(tree_str): """Parse Newick tree string and return a list of species names.""" tree_str = get_clean_tree_str(tree_str) if not tree_str: return [] re_all_names = re.compile(RE_FIND_ALL_SPECIES) all_names = re_all_names.findall(tree_str) species_names = [] for i, name in enumerate(all_names): name = name.strip() if not name: continue if name[0] in NON_SPECIES_NAME_STARTING_CHAR_SET: continue if ':' in name: species_names.append(name.split(':')[0]) if '#' in name: species_names.append(name.split('#')[0]) elif len(name.split()) != 1: continue else: species_names.append(name) for each_char in NON_SPECIES_NAME_STARTING_CHAR_SET: species_names = [_ for _ in all_names if each_char not in _] return species_names def check_all_names_in_newick_tree(tree_str, cali_tuple_list): """Check whether all names from config lines are in Newick tree.""" all_names_from_tree_str = get_species_names_from_tree_str(tree_str) for index, each_tuple in enumerate(cali_tuple_list): if len(each_tuple) == 3: name_a, name_b = [_.strip() for _ in each_tuple[:2]] for name in (name_a, name_b): if name not in all_names_from_tree_str: sys.stderr.write( 'Name not found in Newick tree: %s (Line: %s)\n' % (name, index+1)) return False elif len(each_tuple) == 2: name = each_tuple[0].strip() if name not in all_names_from_tree_str: sys.stderr.write( 'Name not found in Newick tree: %s (Line: %s)\n' % (name, index+1)) return False return True def main(): """Main GUI Application.""" app = App() app.mainloop() if __name__ == '__main__': main()