#!/usr/bin/env python2
# -*- coding: utf-8 -*-
#
# Copyright 2012 Unknown <diogo@arch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
# Standard libraries imports
from os.path import dirname, exists, splitext
from collections import OrderedDict
from functools import partial
from copy import deepcopy
from scipy import misc
import multiprocessing
import matplotlib.patches as patches
import subprocess
import platform
import logging
import ctypes
import pickle
import urllib
import string
import signal
import shutil
import Queue
import time
import stat
import sys
import os
import threading
# Move to Application's directory. This is a way of avoiding encoding
# issues when the full path to the application's directory contains
# non-ASCII characters. This way, the cur_dir attribute will be always
# set to "."
called_dir = os.getcwd()
if getattr(sys, "frozen", False):
os.chdir(dirname(sys.executable))
elif __file__:
os.chdir(dirname(os.path.abspath(__file__)))
# freeze_support must be called here so that multiprocessing work
# correctly on windows
multiprocessing.freeze_support()
# Bypass default argument handling from kivy
os.environ["KIVY_NO_ARGS"] = "1"
# Comment this section until the EventLoop for sphinx building.
# Kivy imports
# from kivy.config import Config
#
# # Sets some kivy configurations before creating main window
# Config.set("kivy", "desktop", 1)
# Config.set("kivy", "exit_on_escape", 0)
# Config.set("graphics", "resizable", 1)
# Config.set("graphics", "fullscreen", 0)
# Config.set("graphics", "minimum_height", 700)
# Config.set("graphics", "minimum_width", 1000)
# Config.set("graphics", "height", 700)
# Config.set("graphics", "width", 1000)
# Config.set("graphics", "multisamples", 4)
# Config.set("input", "mouse", "mouse, disable_multitouch")
#
# # Force creation of main window
# from kivy.base import EventLoop
# EventLoop.ensure_window()
from kivy.app import App
from kivy.animation import Animation
from kivy.uix.checkbox import CheckBox
from kivy.uix.scrollview import ScrollView
from kivy.lang import Builder
from kivy.properties import ListProperty, DictProperty
from kivy.clock import Clock
from kivy.uix.treeview import TreeViewLabel
# Local TriFusion imports
try:
from __init__ import __version__, __build__
import data.resources.theme.default as tm
from ortho import protein2dna
from process.base import Base
from process.data import Partitions, InvalidPartitionFile
from process.error_handling import EmptyAlignment, EmptyData, KillByUser, \
SingleAlignment
from process.sequence import AlignmentList
from data.resources.info_data import orthology_plots, \
informative_storage
from data.resources.background_tasks import remove_tmp, \
load_proc, get_stats_summary, background_process, \
background_export_groups, orto_execution, process_execution, \
orto_update_filters, get_active_group, get_orto_data, get_stats_data
from data.resources.custom_widgets import *
from data.resources.stats import *
from base.plotter import scatter_plot, bar_plot, \
interpolation_plot, stacked_bar_plot, box_plot, histogram_smooth, \
histogram_plot, triangular_heat, outlier_densisty_dist, \
sliding_window
from base.html_creator import HtmlTemplate
from ortho.OrthomclToolbox import MultiGroupsLight
except ImportError:
from trifusion import __version__, __build__
import trifusion.data.resources.theme.default as tm
from trifusion.ortho import protein2dna
from trifusion.process.base import Base
from trifusion.process.data import Partitions, InvalidPartitionFile
from trifusion.process.error_handling import EmptyAlignment, EmptyData, \
KillByUser, SingleAlignment
from trifusion.process.sequence import AlignmentList
from trifusion.data.resources.info_data import orthology_plots, \
informative_storage
from trifusion.data.resources.background_tasks import remove_tmp, \
load_proc, get_stats_summary, background_process, \
background_export_groups, orto_execution, process_execution, \
orto_update_filters, get_active_group, get_orto_data, get_stats_data
from trifusion.data.resources.custom_widgets import *
from trifusion.data.resources.stats import *
from trifusion.base.plotter import scatter_plot, bar_plot, \
interpolation_plot, stacked_bar_plot, box_plot, histogram_smooth, \
histogram_plot, triangular_heat, outlier_densisty_dist, \
sliding_window
from trifusion.base.html_creator import HtmlTemplate
from trifusion.ortho.OrthomclToolbox import MultiGroupsLight
###################################
# Modifications to kivy source code
###################################
# MEMO 1
# scatter.py on_touch_up function was modified to prevent a bug from
# crashing the app. Before the fix, when right-clicking in a
# ScatterLayout and then left-clicking in the generate ball would crash
# the app with a KeyError exception. The fix on lines 595-598 handles
# this exception by not deleting a touch from _last_touch_pos
# dictionary, since the key is not there anymore.
# MEMO 2
# scrollview.py _change_touch_mode function was modified to prevent a bug
# from # crashing the app. Before the fix, sometimes the app would crash
# due to a KeyError exception on line 920. I did not detect any
# specific pattern for this error and I could never replicate the bug,
# but every now and then the app would crash. The fix on lines 920-923
# handles the KeyError exception by returning the function.
# MEMO 3
# To avoid issues with non-ascii characters on the path of the binary
# file, some changes were performed on matplotlib and kivy files.
# .:Matplotlib:.
# .: matplotlib/__init__.py :.
# [l324](wrap()) Wrap the self.report() method around a try
# statement and handle all exceptions attributing False to spoke
# [l822](matplotlib_fname) hardcode the join() method with "mpl-data"
# .: matplotlib/font_manager.py:.
# [l1026](__init__()) Replace the "datapath" value of the rcParams
# dictionary wit a relative path ->
# rcParams["datapath"] = os.path.relpath(rcParams["datapath"])
# .: matplotlib/mathtext.py:.
# [l1013](StandardPsFonts()) Wrap the get_data_path() function call
# with a relative path ->
# basepath = os.path.join( os.path.relpath(get_data_path()),
# 'fonts', 'afm' )
# .: matplotlib/style/core.py:.
# [l30] Same as previous ->
# BASE_LIBRARY_PATH = os.path.join(
# os.path.relpath(mpl.get_data_path()), 'stylelib')
# .:Kivy:.
# .: kivy/atlas.py :.
# [l206] Wrap the dirname(filename) in a relative path ->
# d = os.path.relpath(dirname(filename))
# .: kivy/core/text/text_sdl2.py :.
# [l21](_get_font_id()) Replace the "font_name_r" value of the
# self.options dictionary with a relative path ->
# self.options['font_name_r'] = os.path.relpath(
# self.options['font_name_r'])
# .: kivy/core/window/window_sdl2.py :.
# [l339](set_icon()) Wrap the filename variable in a relative path ->
# self._win.set_window_icon(str(os.path.relpath(filename)))
# ==============================================================================
# EXCEPTIONS
# ==============================================================================
MCL_FILE = None
[docs]class TriFusionApp(App):
#######################
#
# GUI RELATED VARIABLES
#
#######################
# Referecence to blue and red colors
_blue = tm.c_app_blue
_red = tm.c_app_red
show_side_panel = BooleanProperty(False)
"""
bool controlling the toggling of main headers. Is set to True
when the sidepanel is open and False when not.
"""
lock_side_panel = BooleanProperty(False)
"""bool that prevents the animation of the sidepanel when True."""
screen = None
"""
Currently open Screen object.
"""
# Variable containing screen names
screen_names = ListProperty()
"""List of basename screen names."""
available_screens = ListProperty()
"""List of paths to screen files."""
loaded_screens = {}
"""
Dictionary that maps the screen name to a Screen object. Entries are
only populated when Screens are loaded for the first time. This
allows the state of each screen to be saved.
"""
plot_screens = ListProperty()
"""List of screens dedicated for plots."""
# Attributes to know current and previous screen
current_screen = StringProperty()
"""String with the name of the current Screen object."""
previous_screen = StringProperty()
"""String with the name of the last Screen object."""
index = NumericProperty(-1)
"""
Integer with the index of the current screen. The index is
retrieved from the :attr:`~.TriFusionApp.screen_names` attribute.
"""
temp_dir = StringProperty()
"""String with path to temporary directory."""
log_file = StringProperty()
"""String with path to TriFusion's log file."""
bm_file = StringProperty()
"""String with path to bookmarks file."""
projects_file = StringProperty()
"""String with path to projects file."""
cur_dir = "."
"""
String with the current working directory. Used to fetch the kivy
screen files.
"""
file_list = []
"""
List with the paths of alignment files loaded into TriFusion.
This attribute should not be modified unless the files are removed
from the application.
"""
active_file_list = ListProperty()
"""
List with the paths of the active alignment files loaded into TriFusion.
Can be changed during the session.
"""
filename_map = DictProperty()
"""
Dictionary mapping the basename of alignment files to their full path.
Some parts of TriFusion need only the basename of the alignment files,
so this attributes allows the conversion.
"""
active_taxa_list = ListProperty()
"""
List with the active taxa loaded into TriFusion. Can be changed
during the session.
"""
home_path = unicode(expanduser("~"))
"""String with path to home directory."""
bookmarks = [[], {}]
"""
List with information of the bookmarks for the filechoosers. The first
element is a list with the full path of the bookmarks. The second
element is a dictionary mapping the full path to the bookmark name.
"""
bookmarks_bt = []
"""
List of bookmark Button widgets. Used for mouse over purposes.
"""
drag_files = []
"""
List that stores the paths of files drag and dropped into TriFusion's
window.
"""
drag_c = 0
"""
Integer that works as a counter of the number of files that were dropped
into TriFusion's window.
"""
_popup = ObjectProperty(None)
"""Reference to Popup object, if open."""
_subpopup = ObjectProperty(None)
"""Reference to sub Popup object, if open."""
_exit_popup = ObjectProperty(None)
"""Reference to exit Popup object, if open"""
# Dictionary containing the values for the main process operations
main_operations = DictProperty({
"concatenation": False,
"conversion": False,
"reverse_concatenation": False})
"""
Dictionary mapping the main operations to a bool value. The active
main operation, if any, has a True value. Only one True value can
exist for the main operations.
"""
# Dictionary containing all values of the switches and checkboxes in the
# process screen
secondary_operations = OrderedDict([("collapse", False),
("filter", False),
("gcoder", False),
("consensus", False)])
"""
Dictionary mapping the secondary operations to a bool value. Active
secondary operations have a True value.
"""
secondary_options = DictProperty([("interleave", False),
("zorro", False),
("collapse_file", False),
("collapse_filter", False),
("filter_file", False),
("taxa_filter", False),
("codon_filter", False),
("gap_filter", False),
("minimum_taxa_filter", False),
("variation_filter", False),
("gcoder_file", False),
("consensus_file", False),
("consensus_single", False)])
"""
Dictionary mapping secondary options to a bool value. Active secondary
options have a True value.
"""
file_overwrite = None
"""
Attribute controlling whether output files should be overwritten
or skipped. Has a None value when inactive, "overwrite" when there is
an instruction to overwrite, and "skip" when there is a instruction to
skip.
"""
file_apply_all = False
"""
bool attribute that determines whether the behaviour of
:attr:`~.TriFusionApp.file_overwrite` should be applied to all files
(True) or not (False).
"""
ortho_search_options = None
"""
Reference to the
:class:`trifusion.data.resources.custom_widgets.OrthologySearchGrid
object, containing the additional option's widgets of the Orthology
Search screen.
"""
orto_search_height = None
"""
Integer with the height property of the
:attr:`~.TriFusionApp.ortho_search_options` object.
"""
# Attribute for the gridlayout widget that will contain all main options
# for the process module
process_grid_wgt = None
"""
Reference to the
:class:`trifusion.data.resources.custom_widgets.ProcessGeneral object
with the general options widgets of the Process screen.
"""
process_options = None
"""
Reference to the
:class:`trifusion.data.resources.custom_widgets.AdditionalProcessContents
object with the additional options widgets of the Process screen.
"""
process_height = None
"""
Integer with the height property of the
:attr:`~.TriFusionApp.process_options` object.
"""
stats_summary = None
"""
Reference to the
:class:`trifusion.data.resources.custom_widgets.StatsSummary object
containing the widgets of the overall summary statistics in the Statistics
screen.
"""
stats_table = None
"""
Reference to the
:class:`trifusion.data.resources.custom_widgets.GeneTable object
containing the widgets of the gene table summary statistics in the
Statistics screen.
"""
MAX_TABLE_N = 50
"""
Integer with the maximum number of rows allowed for the gene table
in the :class:`trifusion.data.resources.custom_widgets.GeneTable object.
"""
gene_table_selection = []
"""
Pandas DataFrame with the selection of gene summary statistics for the
:class:`trifusion.data.resources.custom_widgets.GeneTable object.
"""
gene_master_table = []
"""
List with the table data from the
:class:`trifusion.data.resources.custom_widgets.GeneTable object.
"""
# Attribute for the widget containing the treeview showing the
# operations queue
operation_tv = ObjectProperty(None)
"""TreeView object containing the queue of operations."""
main_nodes = DictProperty()
"""
Dictionary that maps string identifiers of main nodes to
those Node objects.
"""
# Attributes containing plot related elements
plot_backups = {}
"""
Dictionary that maps Statistics plot analyses identifiers to their
respective table data in a list object. This object is only populated
when those analyses are executed.
"""
#TODO: Check memory usage of plot_backups
current_plt_idx = ListProperty(None)
"""
List that contains two elements. The first is the string identifier
of the current plot analyses. The second is the list of arguments
that should be passed to the respective plotting method.
"""
current_plot = ObjectProperty(None, allownone=True)
"""
matplotlib.Figure object of the current plot.
"""
current_lgd = None
"""matplotlib.Legend object associated to the current plot. Can be None."""
current_table = ObjectProperty(None, allownone=True)
"""List with table data associated with the current plot."""
# Patch attribute
plt_patch = None
"""Stores matplotlib.patches objects that may be added to plots."""
orto_plt_method = {
"Taxa distribution":
[bar_plot, "Species_distribution.png"],
"Gene copy distribution":
[bar_plot, "Gene_copy_distribution.png"],
"Taxa coverage":
[bar_plot, "Species_coverage.png"],
"Taxa gene copies":
[bar_plot, "Species_copy_number.png"]
}
"""
Dictionary that maps the string identifier of orthology plot analyses
to a list. This list has two elements. The first is the plotting
method defined in :mod:`trifusion.base.plotter`, and the second is
the name of the temporary figure file that will be generated.
"""
stats_plt_method = {
"Gene occupancy":
[interpolation_plot, "gene_occupancy.png"],
"Distribution of missing data sp":
[stacked_bar_plot, "missing_data_distribution_sp.png"],
"Distribution of missing data":
[histogram_smooth, "missing_data_distribution.png"],
"Distribution of missing orthologs":
[bar_plot, "missing_gene_distribution.png"],
"Distribution of missing orthologs avg":
[histogram_plot, "missing_gene_distribution_avg.png"],
"Cumulative distribution of missing genes":
[bar_plot, "cumulative_distribution_missing_genes.png"],
"Distribution of sequence size":
[box_plot, "avg_seqsize_species.png"],
"Distribution of sequence size all":
[histogram_plot, "avg_seqsize.png"],
"Proportion of nucleotides or residues":
[bar_plot, "char_proportions.png"],
"Proportion of nucleotides or residues sp":
[stacked_bar_plot, "char_proportions_sp.png"],
"Pairwise sequence similarity":
[histogram_plot, "similarity_distribution.png"],
"Pairwise sequence similarity sp":
[triangular_heat, "similarity_distribution_sp.png"],
"Pairwise sequence similarity gn":
[sliding_window, "similarity_distribution_gn.png"],
"Segregating sites":
[histogram_plot, "segregating_sites.png"],
"Segregating sites prop":
[histogram_plot, "segregating_sites_prop.png"],
"Segregating sites sp":
[triangular_heat, "segregating_sites_sp.png"],
"Segregating sites gn":
[sliding_window, "segregating_sites_gn.png"],
"Alignment length/Polymorphism correlation":
[scatter_plot, "length_polymorphism_correlation.png"],
"Distribution of taxa frequency":
[histogram_plot, "distribution_taxa_frequency.png"],
"Allele Frequency Spectrum":
[histogram_plot, "allele_frequency_spectrum.png"],
"Allele Frequency Spectrum prop":
[histogram_plot, "allele_frequency_spectrum_prop.png"],
"Allele Frequency Spectrum gn":
[histogram_plot, "allele_frequency_spectrum_gn.png"],
"Missing data outliers":
[outlier_densisty_dist, "Missing_data_outliers.png"],
"Missing data outliers sp":
[outlier_densisty_dist, "Missing_data_outliers_sp.png"],
"Segregating sites outliers":
[outlier_densisty_dist, "Segregating_sites_outliers.png"],
"Segregating sites outliers sp":
[outlier_densisty_dist, "Segregating_sites_outliers_sp.png"],
"Sequence size outliers":
[outlier_densisty_dist, "Sequence_size_outliers.png"],
"Sequence size outliers sp":
[outlier_densisty_dist, "Sequence_size_outliers_sp.png"]
}
"""
Dictionary that maps the string identifier of statistics plot analyses
to a list. This list has two elements. The first is the plotting
method defined in :mod:`trifusion.base.plotter`, and the second is
the name of the temporary figure file that will be generated.
"""
previous_sets = {"Files": [], "Taxa": [], "Stats": []}
"""
Dictionary storing previous data sets defined in TriFusion. This is
used to evaluate whether the plot methods should be executed or ignored.
The Stats key refers to the previous active data sets when the stats
summary statistics overview was performed. It should contain two lists,
the first with the file set and the second with the taxa set.
previous_sets = {"Files": [], "Taxa": [], "Stats": []}
"""
previous_stats_toggle = None
"""
Reference to
:class:`~trifusion.data.resources.custom_widgets.StatsToggleWgt object.
It's used to restore the widget to the Statistics screen when exiting
and entering it when a plot is loaded.
"""
# Attributes for storing taxa and file buttons for side panel. These
# will be used when search for files/taxa and for loading only
# button subsets for very large data sets. Each list element
# pertains a single file/taxon and it will be a tupple containing the
# main button, information button and remove button.
sp_file_bts = ListProperty()
"""
List of widgets from individual files in the "Files" tab of the
sidepanel. This attribute is used when searching for files and for
loading only Button subsets of very large data sets. Each element
will be a tuple containing the main Button, the information Button,
and the remove Button widgets.
"""
sp_taxa_bts = ListProperty()
"""
List of widgets from individual taxa in the "Taxa" tab of the
sidepanel. This attribute is used when searching for taxa and for
loading only Button subsets of very large data sets. Each element
will be a tuple containing the main Button, the information Button,
and the remove Button widgets.
"""
sp_partition_bts = ListProperty()
"""
List of widgets from individual partitions in the "Partitions" tab of the
sidepanel. This attribute is used when searching for partitions and for
loading only Button subsets of very large data sets. Each element
will be a tuple containing the main Button, the counter Button,
and the edit Button widgets.
"""
# Attributes that control the amount of taxa/file buttons showing at the
# side panel. To avoid staggering the app with tons of buttons, a
# maximum number of buttons showing initially is set. More buttons
# can be later added.
MAX_FILE_BUTTON = NumericProperty(20)
"""
Integer with the maximum number of file Buttons allowed in the "Files"
tab of the sidepanel. Prevents the loading of all file Buttons in
very large datasets.
"""
count_files = NumericProperty(0)
"""
Integer that servers as a counter of the number of file Buttons currently
loaded into the "Files" tab of the sidepanel.
"""
MAX_PARTITION_BUTTON = NumericProperty(20)
"""
Integer with the maximum number of partition Buttons allowed in the
"Partitions" tab of the sidepanel. Prevents the loading of all partition
Buttons in very large datasets.
"""
count_partitions = NumericProperty(0)
"""
Integer that servers as a counter of the number of partition Buttons
currently loaded into the "Partitions" tab of the sidepanel.
"""
mouse_over_bts = DictProperty({
"Files": [],
"Taxa": [],
"Partitions": []})
"""
Attributes storing the ToggleButton objects from Taxa/File/Partitions
tabs in the sidepanel. Used mostly for mouse over events.
"""
previous_mouse_over = StringProperty("")
"""
The Button text property of the previous mouse over event. This will
allow the assessment of whether the current mouse collision is for the
same button (in which case the mouse over will not be triggered) or for
a different button (in which case the mouse over is triggered).
"""
mouse_over_ready = BooleanProperty(True)
"""
bool attribute that is a locking mechanism of the mouse over event.
When there is a scheduled event for a mouse over this attribute is set
to False, which prevents further events from being scheduled in the
meantime. When the scheduled event is dispatched, the lock is released
and it returns to True.
"""
old_mouse_over = None
"""
Label widget of the previous mouse over. Used so that this label can be
removed.
"""
fancy_bt = None
"""
Reference to the
:class:`~trifusion.data.resources.custom_widgets.FancyButton` object
from the current mouse over.
"""
touch = None
"""
Touch object. Can be used to query the type of touch on certain events.
"""
removable_media = []
"""
List attribute that stores paths of currently active removable media.
"""
arrow_block = BooleanProperty(False)
"""
bool attribute that ocks arrow keys keybindings when a text input is
focused.
"""
# Whether SidePanel's More options dialog is active or not
sp_moreopts = BooleanProperty(False)
"""
bool attribute of whether the side panel's MoreOptions dialog is active
(True) or not (False).
"""
# Whether a FancyDropDown is... well... dropped
fancy_dropped = BooleanProperty(False)
"""
bool attribute of whether the
:class:'trifusion.data.resources.custom_widgets.FancyDropDown object
if active (True) or not (False).
"""
last_sp_bt = {"Files": None, "Taxa": None, "Partitions": None}
"""
Dictionary that stores the last selected Button object in the sidepanel
for Files, Taxa and Partitions.
last_sp_bt = {"Files": None, "Taxa": None, "Partitions": None}
"""
is_control_pressed = BooleanProperty(False)
"""
bool attribute that stores information on whether the control key
is being pressed (True) or not (False).
"""
is_shift_pressed = BooleanProperty(False)
"""
bool attribute that stores information on whether the shift key
is being pressed (True) or not (False).
"""
prev_tb = None
"""
Attribute that stores the previous Button for general purpose multi
selection of toggle buttons using shift clicking
"""
mouse_position = ListProperty()
"""
List of the mouse position coordinates in [xpos, ypos].
"""
lock_stats = BooleanProperty(False)
"""
bool attribute that determines whether statistics background processes
should be interrupted (True) or not (False).
"""
# Attributes used to determine is a specific background process
# should be terminated by the user. All values are set to True by
# default and their values only change on the start up of the
# appropriate function.
terminate_stats = BooleanProperty(True)
"""
bool attribute used to determine if a summary statists background
process should be terminated by the user. Value is True by default and
they only change on the start up of the appropriate background function.
"""
terminate_process_exec = BooleanProperty(True)
"""
bool attribute used to determine if a process execution background
process should be terminated by the user. Value is True by default and
they only change on the start up of the appropriate background function.
"""
terminate_background = BooleanProperty(True)
"""
bool attribute used to determine if a generic background
process should be terminated by the user. Value is True by default and
they only change on the start up of the appropriate background function.
"""
terminate_load_files = BooleanProperty(True)
"""
bool attribute used to determine if a file loading background
process should be terminated by the user. Value is True by default and
they only change on the start up of the appropriate background function.
"""
terminate_orto_search = BooleanProperty(True)
"""
bool attribute used to determine if a orthology search background
process should be terminated by the user. Value is True by default and
they only change on the start up of the appropriate background function.
"""
terminate_group_export = BooleanProperty(True)
"""
bool attribute used to determine if a ortholog export background
process should be terminated by the user. Value is True by default and
they only change on the start up of the appropriate background function.
"""
################################
#
# CORE PROGRAM RELATED VARIABLES
#
################################
sqldb = StringProperty("")
"""String with path to sqlite database."""
ortho_dir = StringProperty("")
"""String with path to OrthoMCL output directory."""
orto_export_dir = StringProperty("")
"""String with path to directory for ortholog exports."""
usearch_file = StringProperty("")
"""String with path to usearch executable."""
usearch_db = StringProperty("goodProteins_db")
"""String with name of usearch's database file."""
usearch_output = StringProperty("AllVsAll.out")
"""String with name of the usearch output file."""
usearch_evalue = StringProperty("0.00001")
"""String with evalue for usearch execution."""
# MCL/Groups attributes
mcl_file = StringProperty("")
"""String with path to mcl executable."""
ortholog_prefix = StringProperty("My_group")
"""String with prefix for ortholog groups."""
group_prefix = StringProperty("group")
"""String with prefix for orthology search group files."""
mcl_inflation = ListProperty(["3"])
"""List with inflation values, as strings, for the mcl execution."""
# Protein quality filters
protein_min_len = NumericProperty(10)
"""Integer with the minimum protein length for OrthoMCL."""
protein_max_stop = NumericProperty(20)
"""Integer with the maximum percentage of stops for OrthoMCL."""
# Orthology cluster filters
orto_max_gene = NumericProperty(1)
"""
Integer with maximum number of gene copies allowed for each
ortholog group.
"""
orto_min_sp = NumericProperty(3)
"""
Integer with the minimum taxa representation allowed for each
ortholog group..
"""
# Attributes for exporting groups as protein/nucleotides
protein_db = StringProperty("")
"""String with path to protein database file."""
cds_db = ListProperty("")
"""List with paths to the CDS files corresponding to proteomes."""
# Attribute containing the path to the proteome files
proteome_files = []
"""List of paths to proteome files loaded into TriFusion."""
active_proteome_files = ListProperty()
"""List of active proteome files."""
# Attribute containing the orthology group files
ortho_group_files = ListProperty()
"""List with paths to ortholog group files loaded into TriFusion."""
ortho_groups = None
"""
Reference to a :class:`~trifusion.ortho.OrthomclToolbox.MultiGroupLight`
object.
"""
active_group = None
"""
Reference to a :class:`~trifusion.ortho.OrthomclToolbox.GroupLight`
object, currently active.
"""
active_group_name = None
"""
String with name of the currently active
:class:`~trifusion.ortho.OrthomclToolbox.GroupLight`.
"""
alignment_list = None
"""
Reference to a :class:`~trifusion.process.sequence.AlignmentList` object.
"""
active_partitions = ListProperty()
"""
List of currently active partitions.
"""
trigger_taxa_update = False
"""
bool attribute that determined whether taxa information needs to be
updated (True) or not (False).
"""
original_tx_inf = DictProperty()
"""
Dictionary that maps a taxon string to the several informative
properties for the entire loaded data set, shown when clicking the
information button in the sidepanel.
"""
active_tx_inf = DictProperty()
"""
Dictionary that maps a taxon string to the several informative
properties for the active data set, shown when clicking the
information button in the sidepanel.
"""
original_file_inf = DictProperty()
"""
Dictionary that maps a filename string to the several informative
properties for the entire loaded data set, shown when clicking the
information button in the sidepanel.
"""
active_file_inf = DictProperty()
"""
Dictionary that maps a filename string to the several informative
properties for the active data set, shown when clicking the
information button in the sidepanel.
"""
#TODO: must check RAM usage of these dictionaries
export_mode = None
"""
Tuple attribute that determines the export mode in the
sidepanel. The first element can be either 'taxa' or 'file'. The
second element can be 'all' or 'select'.
"""
sequence_types = StringProperty()
"""
String attribute storing the sequence type of alignment files
currently loaded.
"""
# Attribute for taxa and file groups
taxa_groups = DictProperty()
"""
Dictionary storing custom taxa groups defined in TriFusion. Keys are
group names and values are a list of taxon names.
"""
file_groups = DictProperty()
"""
Dictionary storing custom file groups defined in TriFusion. Keys are
group names and values are a list of filenames.
"""
dataset_file = None
"""
String attribute with path to a data set file.
"""
output_file = StringProperty("")
"""
String with path to output file for Process execution.
"""
output_dir = StringProperty("")
"""String with path to output directory for Process execution."""
conversion_suffix = StringProperty("")
"""
String with suffix to add for conversion operation of Process
execution.
"""
output_formats = ListProperty(["fasta"])
"""
List of output currently selected output formats for Process execution.
"""
ld_hat = BooleanProperty(False)
"""
bool attribute that determines whether the Fasta output format shoud
be compliant with LDhat (True) or a regular fasta (False).
"""
# Determines whether the part.File associated with phylip format is
# created
create_partfile = BooleanProperty(True)
"""
bool attribute that determines whether the partition file associated with
the phylip format should be created (True) or not (False).
"""
use_nexus_partitions = BooleanProperty(True)
"""
bool attribute that determines whether the charset partitions block
in nexus format should be written (True) or not (False).
"""
use_nexus_models = BooleanProperty(True)
"""
bool attribute that determines whether the substitution models
associated to alignment partitions should be written to the nexus
file (True) or not (False).
"""
phylip_truncate_name = BooleanProperty(False)
"""
bool attribute that determines whether taxa names should be truncated
to 10 characters (True) or not (False) in a phylip output file.
"""
ima2_options = ListProperty([None, None, None, None])
"""
Four element list of additional options for IMa2 format:
1. str - population file path
2. str - Population tree string
3. list - mutation model for each partition (one element if
applies to all)
4. list - inheritance scalaer for each partition (one element if
applies to all)
"""
missing_filter_settings = ListProperty([(True, 25, 50), (True, 0)])
"""
Two element list attribute storing the missing data filter settings.
The first element is a three element tuple:
1. bool. Whether the missing data filter is active (True) or not
(False).
2. int. Percentage of gaps allowed.
3. int, Percentage of missing data allowed.
The second element is a two element tuple:
1. bool. Whether the minimum taxa representation filter is active
(True) or not (False).
2. int. Percentage of the min taxa representation
"""
taxa_filter_settings = ListProperty([None, None])
"""
List attribute storing the taxa filter settings. The first element of
the list should be the filter mode (either "Contain" or "Exclude")
and the second element should be a string with the name of the taxa
group (from the :attr:`~TriFusionApp.taxa_group` attribute).
"""
codon_filter_settings = ListProperty([True, True, True])
"""
List attribute storing the alignment filter settings. This will determine
which codon positions will be written to the output (only for DNA
sequences), so this will consist of a list containing 3 elements that
correspond to each position. Positions will be saved or filtered
depending on the boolean value of the list position. Ex. [True,
True, True] will save all positions, whereas [True, True, False]
will only save the first two positions.
"""
variation_filter = ListProperty([None, None, None, None])
"""
List attribute storing the alignment variation filter settings. The
elements correspond to:
1. Minimum variable sites.
2. Maximum variable sites.
3. Minimum informative sites.
4. Maximum informative sites.
"""
use_app_partitions = BooleanProperty(False)
"""
bool atribute determining whether reverse concatenation will use a
partition file (False) or the partitions defined in the app (True).
"""
partitions_file = StringProperty("")
"""
String with path to partitions file.
"""
rev_infile = StringProperty("")
"""
String with nameo the alignment file to be reverse concatenated.
"""
zorro_suffix = StringProperty("")
"""String with the suffix of zorro files."""
zorro_dir = StringProperty("")
"""String with the directory containing the zorro files."""
hap_prefix = StringProperty("Hap")
"""String with the haplotype prefix for collapse operation."""
[docs] def build(self):
# Setting main window title
self.title = "TriFusion - Streamline phylogenomics"
# Set icon
self.icon = join(self.cur_dir, "data", "backgrounds",
"trifusion-icon-512.png")
# Set temporary directory path
self.temp_dir = join(self.user_data_dir, "tmp")
# Set directory for error records
self.log_file = join(self.user_data_dir, "log", "error.out")
# Set path for file hosting filechooser bookmarks
self.bm_file = join(self.user_data_dir, "bookmarks")
# Set path for file hosting the user projects
self.projects_file = join(self.user_data_dir, "projects")
# Set path for sequence data sqlite database
self.sqldb = join(self.temp_dir, "sequencedb")
logging.basicConfig(filename=self.log_file, level=logging.DEBUG,)
# Setting available screens
self.available_screens = ["main", "Orthology", "Process",
"Statistics", "fc", "group_compare",
"plot", "orto_plot"]
self.screen_names = self.available_screens
# Transforming screen names into complete paths to be loaded by kivy
self.available_screens = [join(self.cur_dir, "data", "screens",
"{}.kv".format(screen)) for screen in
self.available_screens]
# Store screen names specifically designed for plot display
self.plot_screens = ["group_compare", "plot", "orto_plot",
"Statistics"]
self.loaded_screens = dict((sc, None) for sc in
self.available_screens)
# First thing is go to main screen
self.go_screen(0)
# Set method for closing side panel when touching outside
Window.bind(on_touch_up=lambda x, y: self.sidepanel_on_touch(y))
# Listen to keybindings
Window.bind(on_key_down=self._on_keyboard_events)
Window.bind(on_key_up=self._release_events)
# Execute cleaning function when exiting app
Window.bind(on_request_close=lambda x:
self.check_action("Are you sure you want to quit?",
self._exit_clean, popup_level=3))
Window.bind(on_stop=lambda x: self._exit_clean())
Window.bind(on_motion=self.mouse_zoom)
Window.bind(on_dropfile=self.load_files_dragndrop)
Window.clearcolor = tm.c_main_background
# Orthology widgets
self.ortho_search_options = OrthologySearchGrid()
# Check for orthology programs
self._check_orto_programs()
# Process widgets
# Creating GridLayout instance for general options of Process
self.process_grid_wgt = ProcessGeneral()
# Create GridLayout instance for additional options of Process.
self.process_options = AdditionalProcessContents()
# Create Summary stats box instance for Statistics screen
self.stats_summary = StatsSummary()
self.stats_table = GeneTable()
# Initialize operation queue treeview in side panel
self.operation_queue_init()
# Initialize projects
self.projects_init()
self.fancy_bt = ObjectProperty(FancyButton())
# Set schedule for mouse over events on side panel
Clock.schedule_interval(lambda x: self._on_mouseover_tabs(), .1)
# Sets schedule for removable media automatic detection
Clock.schedule_interval(lambda x: self._check_removable_media(), 2)
# Set path to sqlite database
self.sqldb = join(self.temp_dir, "trifusion.sql3")
self._start_clean()
# Now that the sqlite database path is defined, initialize the
# AlignmentList object
self.alignment_list = AlignmentList([], sql_db=self.sqldb)
# If arguments were provided at the command line, load data into
# the app assuming they are alignments
if sys.argv[1:]:
Clock.schedule_once(
lambda dt: self.load_files_startup(sys.argv[1:]), .1)
"""
------------------------ METHOD NOMENCLATURE GUIDE -----------------
Given the large number of methods needed to give functionality to
the app, this nomenclature guide was created to aid in the naming
of new methods so that the code can be more easily browsed and
understood. Note that this guide only targets methods that
perform similar tasks and, therefore, can be grouped by a common
prefix name. Other methods that perform more unique operations
may have different names.
Method's names will be given based on their main operation and
specific task. For example, a method in charge of toggle the side
panel, should be named "toggle_sidepanel", being "toggle" the
common prefix and "sidepanel" the keyword linked ot the specific
task.
1. Toggles.
"toggle_[specific_task]", e.g. "toggle_sidepanel"
Methods use to toggle certain widgets or values/attributes.
2. Dialogues.
"dialog_[specific_task]", e.g. "dialog_format"
Methods that generate dialogues throughout the app, usually in
the form of popups
3. Populating methods.
"populate_[specific_task]", e.g., "populate_input_files"
Methods that populate certain widgets, usually gridlayouts, with
other widgets
4. Add/Remove
"add_[specific_task]", e.g., "add_bookmark"
"remove_[specific_task]", e.g., "remove_taxa_group"
Methods that add or remove widgets, usually buttons/togglebuttons,
from other widgets
5. Saves.
"save_[specific_task]", e.g., "save_file"
Methods that save specific settings from the several options of the
sapp
6. Updates.
"update_[specific_task]", e.g., "update_tabs"
Wrapper methods used to update several attributes or widgets of the
app
7. Checks.
"check_[specific_task]", e.g., "check_filters"
Methods that perform some kind of sanity checks to user input data
8. Unique operations
[specific_task]_[unique_operation], e.g., "sidepanel_animation"
When the method performs a unique operations, the specific_task
should prefix the name of the method.
"""
[docs] def mouse_zoom(self, *vals):
"""
:param vals: touch event list
"""
# Only perform any actions in plot screens and pop is not active
if self.screen.name in self.plot_screens and \
self._popup not in self.root_window.children and \
self._subpopup not in self.root_window.children:
# Ignore if in statistics screen and sidepanel expanded
if self.screen.name == "Statistics" and \
self.screen.ids.stats_panel.width == 410:
return
# Add exceptions here
try:
if isinstance(self.screen.ids.plot_content.children[
0].children[0], StatsSummary) or \
isinstance(self.screen.ids.plot_content.children[
0].children[0], GeneTable):
return
except (IndexError, ValueError):
pass
motion = vals[2]
# Check if motion is mouse scroll
if motion.is_mouse_scrolling:
# Only perform an action at the begining of the motion
if vals[1] == "begin" and self.is_control_pressed:
if motion.button == "scrollup":
self.screen.ids.plot_content.scale -= .1
elif motion.button == "scrolldown":
self.screen.ids.plot_content.scale += .1
def _start_clean(self):
"""
In the event of unexpected exits, clean the tmp directory on app
start
:return:
"""
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
os.makedirs(self.temp_dir)
def _check_exec(self, path, program):
"""
Checks the execution of the MCL software from a given path. If the MCL
is correct, returns True
"""
msg = {"mcl": b"mcl",
"usearch": "usearch"}
# If the OS is windows and the program is usearch, first check if the
# necessary DLL vcomp100.dll is present in the app's directory
if sys.platform in ["win32", "cygwin"] and program == "usearch":
flist = [x.lower() for x in os.listdir(self.user_data_dir)]
if "vcomp100.dll" not in flist:
self.dismiss_subpopup()
return self.dialog_warning(
"Missing a DLL file",
"You are providing the USEARCH executable but the"
" vcomp100.dll file is missing. Please download it from"
" the link below and place the DLL in"
" C:\Users\<USER>\AppData\Roaming\\trifusion. Then"
" try again.\n\n"
"[ref=http://drive5.com/usearch/vcomp100.dll][b]"
"[color=#37abc8ff]http://drive5.com/usearch/vcomp100.dll"
"[/b][/color][/ref]")
# First, try to execute the file directly from the path
try:
res, _ = subprocess.Popen([path, "--version"],
stdout=subprocess.PIPE).communicate()
return res.startswith(msg[program])
except OSError as e:
# This exception may occur because the file is not executable
if e.errno == 13:
try:
# Make the file executable
st = os.stat(path)
os.chmod(path, st.st_mode | stat.S_IEXEC)
res, _ = subprocess.Popen(
[path, "--version"],
stdout=subprocess.PIPE).communicate()
return res.startswith(msg[program])
except OSError:
# The provided file is not executable and TriFusion does
# not have permissions to change that.
return False
else:
# The first OSError exception could not be handled. Fail the
# executable check
return False
def _check_orto_programs(self):
"""
Checks if the USEARCH and MCL programs are installed and reachable.
1. Check if executables are in the app_dir
2. Check if executables are callable from subprocess
3. If both checks fail, modify ortho_search_grid to highlight the
problem.
"""
dll_path = None
# The mcl executable requires the .exe extension in windows to
# work properly
if sys.platform in ["win32", "cygwin"]:
mcl_string = "mcl.exe"
else:
mcl_string = "mcl"
# Check if MCL_FILE has been set. This happens when running TriFusion
# from the executable binary.
if MCL_FILE:
self.mcl_file = MCL_FILE
# Check MCL in app_dir
elif os.path.exists(join(self.user_data_dir, mcl_string)):
self.mcl_file = join(self.user_data_dir, mcl_string)
# Check MCL in package directory, if running from source
elif os.path.exists(join(os.getcwd(), "data", "resources", "mcl")):
mcl_dir = join(os.getcwd(), "data", "resources", "mcl")
# For Windows
if sys.platform in ["win32", "cygwin"]:
# For Windows 64bit
if platform.architecture()[0] == "64bit":
mcl_path = join(mcl_dir, "windows", "64bit", "mcl64.exe")
dll_path = join(mcl_dir, "windows", "64bit",
"cygwin1.dll")
# For Windows 32bit
else:
mcl_path = join(mcl_dir, "windows", "32bit", "mcl32.exe")
dll_path = join(mcl_dir, "windows", "32bit",
"cygwin1.dll")
# For MacOS
elif sys.platform == "darwin":
mcl_path = join(mcl_dir, "MacOS", mcl_string)
# For Linux
else:
mcl_path = join(mcl_dir, "linux", mcl_string)
# Copy mcl executable to trifusion dir
shutil.copyfile(mcl_path, join(self.user_data_dir,
mcl_string))
mcl_file = join(self.user_data_dir, mcl_string)
# Copy dll necessary in windows version
if dll_path:
shutil.copyfile(dll_path, join(self.user_data_dir,
"cygwin1.dll"))
# Make mcl executable
st = os.stat(mcl_file)
os.chmod(mcl_file, st.st_mode | stat.S_IEXEC)
# Test mcl executable
if self._check_exec(mcl_file, "mcl"):
self.mcl_file = mcl_file
else:
# Remove bad executable from app dir
os.remove(join(self.user_data_dir, mcl_string))
if dll_path:
os.remove(join(self.user_data_dir, "cygwin1.dll"))
else:
# If not in app_dir check is its reachable system-wide by
# subprocess
try:
subprocess.call(["mcl"])
self.mcl_file = mcl_string
except OSError:
pass
# If the mcl file could not be found through any of the previous
# methods, modify the attributes of the orto_search_grid.
if not self.mcl_file:
self.ortho_search_options.ids.mcl_check.background_src = \
"data/backgrounds/red_noise.png"
self.ortho_search_options.ids.mcl_check_ico.icon_src = \
"data/backgrounds/warning_icon.png"
self.ortho_search_options.ids.mcl_check_ico.icon_size = \
(20, 20)
self.ortho_search_options.ids.mcl_check_lbl.text = \
"MCL is not installed or reachable"
fix_bt = FixButton()
fix_bt.bind(on_release=lambda x: self.dialog_mcl_fix())
self.ortho_search_options.ids.mcl_fix_box.add_widget(fix_bt)
# Check USEARCH DLL in app_dir for windows only
if sys.platform in ["win32", "cygwin"] and \
not os.path.exists(join(self.user_data_dir, "vcomp100.dll")):
# Copy necessary DLL to app dir if it doesn't exist
try:
if platform.architecture()[0] == "64bit":
shutil.copyfile(join(os.getcwd(), "data", "resources",
"usearch", "64bit", "vcomp100.dll"),
join(self.user_data_dir, "vcomp100.dll"))
else:
shutil.copyfile(join(os.getcwd(), "data", "resources",
"usearch", "32bit", "vcomp100.dll"),
join(self.user_data_dir, "vcomp100.dll"))
except IOError:
pass
# CHeck USEARCH in app_dir
if os.path.exists(join(self.user_data_dir, "usearch")):
self.usearch_file = join(self.user_data_dir, "usearch")
else:
# If not in app_dir check is its reachable system-wide by
# subprocess
try:
subprocess.call(["usearch"])
self.usearch_file = "usearch"
# MCL software not reachable. Modify attributes and
# orto_search_grid
except OSError:
self.ortho_search_options.ids.usearch_check.background_src = \
"data/backgrounds/red_noise.png"
self.ortho_search_options.ids.usearch_check_ico.icon_src = \
"data/backgrounds/warning_icon.png"
self.ortho_search_options.ids.usearch_check_ico.icon_size = \
(20, 20)
self.ortho_search_options.ids.usearch_check_lbl.text = \
"USEARCH is not installed or reachable"
fix_bt = FixButton()
fix_bt.bind(on_release=lambda x: self.dialog_usearch_fix())
self.ortho_search_options.ids.usearch_fix.add_widget(fix_bt)
[docs] def dialog_mcl_fix(self):
"""
Opens a dialog with information for fixing the missing MCL executable
"""
content = DialogMCLFix(cancel=self.dismiss_popup)
self.show_popup(title="MCL troubleshooting...", content=content,
size=(400, 330), close_bt=True)
[docs] def dialog_usearch_fix(self):
"""
Opens a dialog with information for fixing the missing USEARCH
executable
"""
content = DialogUSEARCHFix(cancel=self.dismiss_popup)
self.show_popup(title="USEARCH troubleshooting...", content=content,
size=(400, 330), close_bt=True)
[docs] def load_files_startup(self, file_list):
"""
Gives support for loading data into the app when launching the
app via command line and providing the files as arguments. Issues
the same dialog as the load_files_dragndrop to let the user
choose whether the input files are alignments or proteomes
:param file_list: list, with file paths as strings or unicode
"""
content = InputType(cancel=self.dismiss_popup)
content.files = file_list
self.show_popup(title="", content=content,
size=(350, 160),
separator_color=(0, 0, 0, 0),
close_bt=True)
[docs] def load_files_dragndrop(self, *args):
"""
This function gives functionality to the drag and drop feature
that automatically loads files when droped into the app window.
Note that this method will issue different data loding methods
depending on the active screen. Proteome alignments will be opened
in the Orthology screen, while sequence alignment will be opened
in the Process and Statistics screens.
:param args: list, first element is the SDL2 object, second
element is the file path
"""
def drag_check(dt):
"""
In order to support drag and drop of multiple files, a periodic
check is made to assess if the self.drag_files object is
still being populated, or if it reached a static state. Only
when it reaches a static state, will the app issue the
corresponding methods to load all files into the app.
:param dt:
"""
# If the drag_files attribute continues to be populated,
# its lenght will be higher than the current counter. In such
# case, update the counter
if len(self.drag_files) > self.drag_c:
self.drag_c = len(self.drag_files)
# In case only a single file was dragged and dropped,
# this additional scheduled event will allow the single
# file to the loaded. Otherwise, it will do nothing.
Clock.schedule_once(drag_check, .1)
# In this case, the drag_files attribute has reached a static
# size and the loading methods can be issued
elif len(self.drag_files) == self.drag_c:
# Issue methods only when drag_files is populated
if self.drag_files:
content = InputType(cancel=self.dismiss_popup)
content.files = self.drag_files
self.show_popup(title="", content=content,
size=(350, 160),
separator_color=(0, 0, 0, 0),
border_color=tm.c_popup_border,
close_bt=True)
# Load proteomes
# if self.screen.name == "Orthology":
# self.load_proteomes(self.drag_files)
# if self.screen.name in ["Process", "Statistics",
# "main"]:
# self.load_files_subproc(self.drag_files)
# Reset drag counter and drag_files attributes
self.drag_c = 0
self.drag_files = []
# This functionality should only be triggered when in one of the
# main screens
self.dismiss_all_popups()
self.drag_files.append(unicode(args[1], "utf8"))
Clock.schedule_once(drag_check, .1)
def _exit_clean(self):
"""
This method is issued when the application is closed and performs
any necessary clean up operations
"""
self.terminate_stats = True
self.terminate_load_files = True
self.terminate_background = True
self.run_in_background(remove_tmp, self.stop,
[self.temp_dir, self.alignment_list.con],
no_arg2=True, cancel=False,
msg="Cleaning temporary files and "
"exiting...")
def _update_path(self, path):
"""
This method updates the filechooser path when clicking on the path
label
:param path: string, with destination path
"""
# If in filechooser, do this
try:
self.screen.ids.path_bx.children[0].text = path
self.screen.ids.icon_view_tab.path = path
# When in popup, do thissd_filechooser
except AttributeError:
try:
self._popup.content.ids.path_bx.children[0].text = path
self._popup.content.ids.sd_filechooser.path = path
except AttributeError:
self._subpopup.content.ids.path_bx.children[0].text = path
self._subpopup.content.ids.sd_filechooser.path = path
def _release_events(self, *vals):
"""
Method that releases keyboard events that are triggered in
_on_keyboard_events when the key is released
:param vals: input list from on_key_up
"""
key_code = vals[1]
if key_code == 305:
self.is_control_pressed = False
if key_code == 304:
self.is_shift_pressed = False
def _on_keyboard_events(self, *vals):
"""
Methods that listens to keyboard input and triggers events or
changes properties when acceptable keyboard shortcuts are entered
:param vals: input list from on_key_down function
"""
# Get modifier (usually ctrl or shift)
# TODO: The modifier in MacOS is different. Must check on this.
modifier = "".join(vals[-1])
key_code = vals[1]
# Switch between Ctrl for Windows/Linux and Cmd for MacOS as the
# preferred modifier
if sys.platform == "darwin":
mod_key = "meta"
else:
mod_key = "ctrl"
if key_code == 305:
self.is_control_pressed = True
if key_code == 304:
self.is_shift_pressed = True
# ==================================================================
# Popup keybindings
# ==================================================================
def popup_keys(backn, backd, bt1, bt2, bt3=None):
"""
Wrapper function that provides functionality to arrow keys for
navigating through selection buttons in popups
:param backn: string for background normal
:param backd: string for background down
:param bt1: Button widget one (usually for Ok buttons)
:param bt2: Button widget two (usually for Cancel buttons)
:param bt3: Optional button widget three.
"""
if not self.arrow_block:
# This will deal with cases with only two buttons to cycle
if not bt3:
# if left arrow key
if key_code == 276:
bt1.background_normal = backn
bt2.background_normal = backd
# if right arrow key
if key_code == 275:
bt1.background_normal = backd
bt2.background_normal = backn
# if enter key. Dispatch the events of the focused
# button
if key_code == 13:
if bt1.background_normal == backn:
bt1.dispatch("on_release")
else:
bt2.dispatch("on_release")
# This will cycle through three buttons
else:
bt_list = [bt1, bt2, bt3]
idx = [i.background_normal for i
in bt_list].index(backn)
if key_code == 276 and idx > 0:
idx -= 1
if key_code == 275 and idx < 2:
idx += 1
for bt in bt_list:
if bt_list.index(bt) == idx:
bt_list[idx].background_normal = backn
else:
bt.background_normal = backd
if key_code == 13:
bt_on = [i for i in bt_list if
i.background_normal == backn][0]
bt_on.dispatch("on_release")
# ==================================================================
# Popup keybindings
# ==================================================================
if self._exit_popup in self.root_window.children:
if "check_ok" in self._exit_popup.content.ids:
bn = "data/backgrounds/check_ok.png"
bd = "data/backgrounds/check_cancel.png"
ok_bt = self._exit_popup.content.ids["check_ok"]
cancel_bt = self._exit_popup.content.ids["check_cancel"]
popup_keys(bn, bd, ok_bt, cancel_bt)
if self._exit_popup in self.root_window.children:
return
if self._popup in self.root_window.children:
if "find_bx" in self._popup.content.ids:
if modifier == mod_key and key_code == 102:
if self._popup.content.ids.find_bx.height == 30:
Animation(height=0, d=.3, t="out_quart").start(
self._popup.content.ids.find_bx)
self._popup.content.ids.sd_filechooser.f = "*"
self._popup.content.ids.text_filter.text = ""
self._popup.content.ids.text_filter.focus = False
else:
Animation(height=30, d=.3, t="out_quart").start(
self._popup.content.ids.find_bx)
self._popup.content.ids.text_filter.focus = True
if self._subpopup in self.root_window.children:
if "find_bx" in self._subpopup.content.ids:
if modifier == mod_key and key_code == 102:
if self._subpopup.content.ids.find_bx.height == 30:
Animation(height=0, d=.3,
t="out_quart").start(
self._subpopup.content.ids.find_bx)
self._subpopup.content.ids.sd_filechooser.f = "*"
self._subpopup.content.ids.text_filter.text = ""
self._subpopup.content.ids.text_filter.focus = False
else:
Animation(height=30, d=.3,
t="out_quart").start(
self._subpopup.content.ids.find_bx)
self._subpopup.content.ids.text_filter.focus = True
# Toggle path editor
if self._popup in self.root_window.children:
if "path_toggle" in self._popup.content.ids:
if modifier == mod_key and key_code == 108:
st = self._popup.content.ids.path_toggle.state
self._popup.content.ids.path_toggle.state = \
"down" if st != "down" else "normal"
self._popup.content.ids.path_toggle.dispatch("on_release")
if self._popup in self.root_window.children:
if "text_filter" in self._popup.content.ids:
# Ctrl + f toggles focus on find text input field
if modifier == mod_key and key_code == 102:
self._popup.content.ids.text_filter.focus = True
if self._subpopup in self.root_window.children:
if "ok_bt" in self._subpopup.content.ids:
bn = "data/backgrounds/bt_process.png"
bd = "data/backgrounds/bt_process_off.png"
ok_bt = self._subpopup.content.ids["ok_bt"]
cancel_bt = self._subpopup.content.ids["cancel_bt"]
popup_keys(bn, bd, ok_bt, cancel_bt)
elif self._popup in self.root_window.children:
if "check_ok" in self._popup.content.ids:
bn = "data/backgrounds/check_ok.png"
bd = "data/backgrounds/check_cancel.png"
ok_bt = self._popup.content.ids["check_ok"]
cancel_bt = self._popup.content.ids["check_cancel"]
popup_keys(bn, bd, ok_bt, cancel_bt)
# In this case there are three buttons to cicle
elif "apply_bt" in self._popup.content.ids:
bn = "data/backgrounds/bt_process.png"
bd = "data/backgrounds/bt_process_off.png"
ok_bt = self._popup.content.ids["ok_bt"]
cancel_bt = self._popup.content.ids["cancel_bt"]
apply_bt = self._popup.content.ids["apply_bt"]
popup_keys(bn, bd, ok_bt, apply_bt, cancel_bt)
# Only two buttons to cicle
elif "ok_bt" in self._popup.content.ids:
bn = "data/backgrounds/bt_process.png"
bd = "data/backgrounds/bt_process_off.png"
ok_bt = self._popup.content.ids["ok_bt"]
cancel_bt = self._popup.content.ids["cancel_bt"]
popup_keys(bn, bd, ok_bt, cancel_bt)
elif "close_bt" in self._popup.content.ids:
if key_code == 13:
self._popup.content.ids.close_bt.dispatch("on_release")
# ======================================================================
# Filechooser keybindings
# ======================================================================
if self.screen.name == "fc":
# Keybinding ctrl+f that brings focus to the "Find" field in the
# Filechooser screen
if modifier == mod_key and key_code == 102:
self.screen.ids.text_filter.focus = True
# Keybinding ctrl+backspace to clear selection
if modifier == mod_key and key_code == 8:
self.screen.ids.clear_s.dispatch("on_release")
# Add bookmarks with ctrl+d
if modifier == mod_key and key_code == 100:
self.screen.ids.add_bk_bt.dispatch("on_release")
# Toggle manual path writing with ctrl+l
if modifier == mod_key and key_code == 108:
if self.screen.ids.path_toggle.state == "down":
self.screen.ids.path_toggle.state = "normal"
else:
self.screen.ids.path_toggle.state = "down"
self.screen.ids.path_toggle.dispatch("on_release")
# Select all files with ctrl+a
if modifier == mod_key and key_code == 97:
self.screen.ids.icon_view_tab.selection = \
[x for x in self.screen.ids.icon_view_tab.files if not
os.path.isdir(x)]
if self._popup not in self.root_window.children:
# Use arrow keys and enter to navigate through open/cancel
# buttons and selecting them
bn = "data/backgrounds/bt_process.png"
bd = "data/backgrounds/bt_process_off.png"
open_close_bt = self.screen.ids.open_close_bt
open_bt = self.screen.ids.open_bt
cancel_bt = self.screen.ids.cancel_bt
popup_keys(bn, bd, open_bt, open_close_bt, cancel_bt)
# ==================================================================
# General keybindings
# ==================================================================
if not [x for x in self.root_window.children
if isinstance(x, CustomPopup)]:
# Keybinding ctrl+o that opens the Filechooser screen
if modifier == mod_key and key_code == 111:
self.go_screen(self.screen_names.index("fc"))
# Changing main screens between Orthology, Process and
# Statistics
if modifier == mod_key and key_code == 49:
self.root.ids.h_ortho.dispatch("on_release")
self.root.ids.h_ortho.state = "down"
if modifier == mod_key and key_code == 50:
self.root.ids.h_process.dispatch("on_release")
self.root.ids.h_process.state = "down"
if modifier == mod_key and key_code == 51:
self.root.ids.h_stat.dispatch("on_release")
self.root.ids.h_stat.state = "down"
# Toggle side panel (slash)
if key_code == 92:
self.root.ids.ap.dispatch("on_release")
# ==================================================================
# Text input autocompletion
# ==================================================================
# Use tab for auto completion when textinput is focused
path_wgt = None
if key_code == 9:
if "path_bx" in self.screen.ids:
path_wgt = self.screen.ids.path_bx.children[0]
elif self._popup:
if "path_bx" in self._popup.content.ids:
path_wgt = self._popup.content.ids.path_bx.children[0]
if isinstance(path_wgt, TextInput):
path = path_wgt.text
s = self._auto_completion(path)
path_wgt.text = s
def _common_path(self, file_list=None):
"""
Returns a string with the longest common path contained in the
self.file_list dir. This is used by FileChoosers to open the nearest
directory to their input files
"""
if file_list:
fl = file_list
else:
fl = self.file_list
# Return None if there are no input files
if not fl:
return
# Get common path
common_path = ""
for char in zip(*fl):
if len(set(char)) == 1:
common_path += "".join(set(char))
else:
break
# Get nearest directory from common path
while not os.path.isdir(common_path):
common_path = sep.join(common_path.split(sep)[:-1])
return common_path
def _auto_completion(self, path):
"""
Method used for providing auto completion for text input widgets
navigating the os file system
"""
# Check if current path exists. If it does, there is no path to auto
# complete and no action is required.
if os.path.exists(path) or path == "":
return path
# If the path does not exist, get the nearest parent directory and
# use the final string of the path to text for auto completion
else:
original_path = path
s = path.split(sep)[-1]
path = sep.join(path.split(sep)[:-1])
# Get list of contents that may
dirlist = [x for x in os.listdir(path) if x.startswith(s) and
os.path.isdir(join(path, x))]
# If there is only one match in dirlist, return that match
if len(dirlist) == 1:
return join(path, dirlist[0])
# If there are multiple matches in dirlist, return the longest
# common substring
elif len(dirlist) > 1:
return join(path, os.path.commonprefix(dirlist))
else:
self.dialog_floatcheck("Path does not exist",
t="warning")
return original_path
@staticmethod
def _determine_collision(wgt, mp):
"""
Provided a widget, this function determines whether the mouse is
colliding with its window coordinates
:param wgt: ToggleButton widget inside a relative layout
:return: Boolean. True is mouse position collides with wgt
"""
# Retrieving window widget coordinates
window_pos = wgt.to_window(wgt.pos[0], wgt.pos[1])
# Creating dummy widget to determine collision
dummy_wgt = Widget(pos=window_pos, size_hint=(None, None),
size=wgt.size)
return dummy_wgt.collide_point(mp[0], mp[1])
def _check_removable_media(self):
"""
Checks, when a Filechooser dialog is active, for the insertion or
removal of external storage devices, and updates the bookmark
buttons accordingly
"""
def detect_changes():
"""
Detects changes between the app attribute self.removable_media
and the removable media detected in the system. If
differences are found, returns the operation (remove ou add)
and a list of paths
:return: Tuple, 1? element is the operation (add/remove); 2?
element is a list of paths with which to perform the operation
"""
# For linux
if sys.platform in ["linux", "linux2"]:
# Get current removable media
i = subprocess.Popen(["mount | grep -e /media -e /mnt | "
"awk '{print $3}'"], shell=True, stdout=subprocess.PIPE)
removable_media = i.communicate()[0].split("\n")[:-1]
# For MacOS
elif sys.platform == "darwin":
removable_media = [join("/Volumes", x) for x in
os.listdir("/Volumes/")]
# For Windows
elif sys.platform in ["win32", "cygwin"]:
removable_media = re.findall(
r"[A-Z]+:.*$", os.popen("mountvol /").read(), re.MULTILINE)
# Prevent no disk on drive popup error in windows
SEM_FAILCRITICALERRORS = 1
SEM_NOOPENFILEERRORBOX = 0x8000
SEM_FAIL = SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS
kernel32 = ctypes.WinDLL('kernel32')
oldmode = ctypes.c_uint()
kernel32.SetThreadErrorMode(SEM_FAIL, ctypes.byref(oldmode))
else:
return
cur, act = set(removable_media), set(self.removable_media)
if cur == act:
return False
elif cur.difference(act):
return "add", [p for p in cur.difference(act)]
elif act.difference(cur):
return "remove", [p for p in act.difference(cur)]
def update_media(operation, paths, wgt, fc_wgt):
"""
Updates media, according to the operation and list of paths
:param operation: string, Either "add" or "remove"
:param paths: list, containing the paths to the added or removed
:param wgt: Widget, preferentially a gridlayout where the
bookmark buttons will be added/removed
:param fc_wgt: FileChooser widget in which bookmark operations
will be performed
"""
if operation == "add":
for p in paths:
# First, check if the provided path is a directory and
# is reachable. If not, we do not add it as a bookmark
if not os.path.isdir(p):
return
# For windows, get the name of the drive manually
if sys.platform in ["win32", "cygwin"]:
name = os.path.splitdrive(p)[0]
else:
name = None
self.add_bookmark_bt(p, wgt, fc_wgt, rm_bt=False,
name=name)
self.removable_media.append(p)
elif operation == "remove":
for bt in wgt.children:
if bt.id in paths:
self.removable_media.remove(bt.id)
wgt.remove_widget(bt)
if self.screen.name == "fc":
res = detect_changes()
if res:
update_media(res[0], res[1], self.screen.ids.sv_mycomp,
self.screen.ids.icon_view_tab)
elif [x for x in self.root_window.children if
isinstance(x, CustomPopup)]:
# Get popup content
wgt = [x for x in self.root_window.children if
isinstance(x, CustomPopup)][0].content
if isinstance(wgt, SaveDialog) or \
isinstance(wgt, LoadMultipleDialog):
res = detect_changes()
if res:
update_media(res[0], res[1], wgt.ids.sv_mycomp,
wgt.ids.sd_filechooser)
def _on_mouseover_tabs(self):
"""
Provides mouse over events throughout the app. In order to reduce
computations in each cycle, the mouse over events should only be
tested when the appropriate screen/widget is visible
"""
try:
self.mouse_position = self.root_window.mouse_pos
except ReferenceError:
pass
# Get mouse position coordinates
mp = self.root_window.mouse_pos
# Set collision attribute
collision = False
# Set side button list
sidebt_list = [x for x in self.root.ids.side_bt.children if
isinstance(x, ToggleButton)]
def show_label(mouse, wgt):
"""
Use this function with a Clock schedule to delay the
introduction of the label widget. Otherwise, it could become
cumbersome to have an label appearing right after the mouse
colliding with the button
:param mouse: mouse position tuple
:param wgt: Layout widget, containing a descriptive label
"""
# Checking if the current mouse position is the same as the
# mouse position when the mouse over was triggered. This
# ensures that if the user changes the mouse position while
# this event is scheduled, the label will not be added to the
# root_window but the lock in self.mouse_over_ready is removed
if self.root_window.mouse_pos == mouse:
# Add widget to root layout
self.root_window.add_widget(wgt)
# Update old label widget
self.old_mouse_over = wgt
# Update old label text
self.previous_mouse_over = wgt.text
# Unlocking mouse over
self.mouse_over_ready = True
def create_sidebt_wgt(text, p, s):
"""
Creates the label for the sidebt mouseover
:param text: string. Text for the label
:param p: tuple. position of the wgt
:param s: tuple. size of the wgt
"""
clear_mouse_overs()
side_bt = SideLabel(text=text, pos=p, size_hint=(None, None),
size=s, bold=True, border=(0, 0, 0, 0))
return side_bt
def create_fancy_label(text, wgt, lbl_height=30, adjust_pos=False,
c=(0.216, 0.67, 0.784, 1), wgt_pos=None,
wgt_size=None, orientation="horizontal",
line_c=(1, 1, 1, 1)):
"""
Creates a fancy label akin to the mouse over labels in github.
This method is quite versatile as it is able to calculate the
available space for the label and determine its orientation
accordingly. It also starts by removing any other potential
fancy labels.
:param text: string, text that will be displayed in the label
:param wgt: widget that triggered the mouse over
:param lbl_height: int, height of the label
:param adjust_pos: Boolean, If True the position of wgt will
be adjusted to window coordenates; If False, use the origial
position of the wgt. This is only relevant when wgt_pos=None
:param c: tuple. Label color
:param wgt_pos: tuple/list, If not None, this will be the
position used to create the label. Superseeds wgt.pos
:param wgt_size: tuple/list, If not None this will be the size
of the wgt. Superseed wgt.size
:param orientation: string, whether the label will be display in
the same horizontal or vertical plane of the widget
:param line_c: tuple/list, color of the border line and arrow
"""
# If the current mouse position is no longer the same when the
# label was issued, then do nothing
if self.root_window.mouse_pos != mp:
# Unlocking mouse over
self.mouse_over_ready = True
# Cleans any possible mouse overs
clear_mouse_overs()
return
# Cleans any possible mouse overs
clear_mouse_overs()
# Determines which coordinates to use
if wgt_pos:
wgt_pos = wgt_pos
else:
if adjust_pos:
wgt_pos = wgt.to_window(wgt.pos[0], wgt.pos[1])
else:
wgt_pos = wgt.pos
# Determines size attribute
if wgt_size:
wgt_size = wgt_size
else:
wgt_size = wgt.size
# Create label
self.fancy_bt = FancyButton(text=text, height=lbl_height,
id=text, background_color=c)
# Update label texture size, so that we can evaluate the
# available space for the label
self.fancy_bt.texture_update()
# Set border line color
self.fancy_bt.line_clr = line_c
# Determine if there is enough space for the label to be
# properly shown. If not, truncante the label width to 70% of
# window width
if (wgt_pos[0] + wgt_size[0] + 5 +
self.fancy_bt.texture_size[0] + 50 >
self.root.width and wgt_pos[0] - wgt_size[0] - 5 -
self.fancy_bt.texture_size[0] - 50 < 0):
self.fancy_bt.width = self.root.width * .7
self.fancy_bt.text_size = self.fancy_bt.size
self.fancy_bt.halign = "center"
self.fancy_bt.valign = "middle"
self.fancy_bt.texture_update()
self.fancy_bt.height = self.fancy_bt.texture_size[1] + 16
else:
# Determine label size. Add horizontal margin space (10)
self.fancy_bt.size = (self.fancy_bt.texture_size[0] +
10, lbl_height)
# Create horizontal label
if orientation == "horizontal":
# Determine if the label has space to the right. If not,
# flip the orientation
if (wgt_pos[0] + wgt_size[0] + 5 + self.fancy_bt.width <
self.root.width):
# Determine position of arrow widget
point_pos = wgt_pos[0] + wgt_size[0] + 5, wgt_pos[1] + \
wgt_size[1] / 2 - 6
# Determine label position
self.fancy_bt.pos = point_pos[0] + 7, wgt_pos[1] + \
wgt_size[1] / 2 - self.fancy_bt.height / 2
# Create arrow widget with left arrow
point_wgt = FancyMarker(background_normal=join("data",
"backgrounds",
"box_arrow_right.png"),
pos=point_pos, size=(7, 12),
background_color=line_c)
# In case this else code is executed, it means there is no
# space for the label to be shown in the right,
# so it will show to the left
else:
# Determine position of arrow widget
point_pos = wgt_pos[0] - 10, wgt_pos[1] + \
wgt_size[1] / 2 - 6
# Determine label position
self.fancy_bt.pos = point_pos[0] - self.fancy_bt.width,\
wgt_pos[1] + wgt_size[1] / 2 - \
self.fancy_bt.height / 2
# Create arrow widget with left arrow
point_wgt = FancyMarker(background_normal=join("data",
"backgrounds",
"box_arrow_left.png"),
pos=point_pos, size=(7, 12),
id=text,
background_color=line_c)
# For vertical orientation
else:
# For now, show always on top
# Determine position of arrow
point_pos = [wgt_pos[0] + (wgt_size[0] / 2),
wgt_pos[1] + wgt_size[1] + 5]
# Determine position of label
self.fancy_bt.pos = [point_pos[0] -
(self.fancy_bt.width / 2) + 6, point_pos[1] + 7]
# Create arrow widget with down arrow
point_wgt = FancyMarker(background_normal=join("data",
"backgrounds",
"box_arrow_down.png"),
pos=point_pos, size=(12, 7),
id=text,
background_color=line_c)
for w in [self.fancy_bt, point_wgt]:
self.root_window.add_widget(w)
# Unlocking mouse over
self.mouse_over_ready = True
def clear_mouse_overs():
"""
Clears fancy mouseovers, if any
"""
for i in [i for i in self.root_window.children
if isinstance(i, FancyButton) or
isinstance(i, FancyMarker)]:
self.root_window.remove_widget(i)
# Only do this routine when the filechooser screen is on
if self.screen.name == "fc" and self.mouse_over_ready and \
self.show_side_panel is False:
case_bt = self.screen.ids.case_bt
for bt in self.bookmarks_bt + [case_bt]:
if self._determine_collision(bt, mp):
collision = True
if bt == case_bt:
if "Case sensitive" not in self.previous_mouse_over:
if case_bt.state == "down":
create_fancy_label("Case sensitive is ON",
case_bt,
orientation="vertical")
else:
create_fancy_label("Case sensitive is OFF",
case_bt,
orientation="vertical")
else:
# Saving relevant attributes, otherwise they would
# be lost
txt = bt.id
pos = bt.to_window(bt.pos[0], bt.pos[1])
size = bt.size
# If the current collision is different from the
# existing fancy lavel, remove it
if [txt] != [x.text for x in
self.root_window.children if
isinstance(x, FancyButton)]:
clear_mouse_overs()
Clock.schedule_once(lambda i: create_fancy_label(
txt, bt, adjust_pos=True, wgt_pos=pos,
wgt_size=size), .8)
self.mouse_over_ready = False
# If no collision is detected, remove any remaining label
# widget
if collision is False and \
self.old_mouse_over in self.root_window.children:
self.root_window.remove_widget(self.old_mouse_over)
# Only do this routine if the side panel is open
if self.show_side_panel and self.mouse_over_ready \
and not self.sp_moreopts:
# Get active tab in side panel
active_tab = self.root.ids.main_tp.current_tab.text
# Get remove all button
rm_bt = [self.root.ids.rm_all_File, self.root.ids.rm_all_Taxa]
part_bts = [self.root.ids.merge_part, self.root.ids.split_part,
self.root.ids.add_part]
# Iterate over buttons of active tab
for bt in self.mouse_over_bts[active_tab] + sidebt_list + \
rm_bt + part_bts:
# Determine if there is a collision with mouse position
if self._determine_collision(bt, mp) and self._popup not in\
self.root_window.children:
if bt in self.mouse_over_bts[active_tab]:
if self._determine_collision(self.root.ids.sv_file,
mp)\
or self._determine_collision(
self.root.ids.sv_sp, mp):
collision = True
else:
continue
else:
# Set collision marker to true
collision = True
# This will determine if a new label button will be
# added to the layout, based on the text of the
# button. If the text is already in the previous
# mouse over, then do nothing. If the text is some
# new button, then do something
if bt in self.mouse_over_bts[active_tab] + sidebt_list:
if bt.text != self.previous_mouse_over:
# Check if there is an old label button and
# remove it
if self.old_mouse_over:
self.root_window.remove_widget(
self.old_mouse_over)
if bt in sidebt_list:
pos = (bt.center_x + bt.width * .5,
bt.center_y - bt.height * .5)
size = (200, bt.height)
label = create_sidebt_wgt(bt.att, pos, size)
show_label(mp, label)
elif bt in self.mouse_over_bts[active_tab]:
# Saving relevant attributes, otherwise they
# would be lost
txt = bt.text
pos = bt.to_window(bt.pos[0], bt.pos[1])
size = bt.size
# If the current collision is different from the
# existing fancy lavel, remove it
if [txt] != [x.text for x in
self.root_window.children if
isinstance(x, FancyButton)]:
clear_mouse_overs()
# Schedule the introduction of the label widget
Clock.schedule_once(lambda i:
create_fancy_label(txt, bt, wgt_pos=pos,
wgt_size=size,
c=(.3, .3, .3, .95)), 1)
# Locking mouse over so that no additional label
# widgets are added during the waiting time
self.mouse_over_ready = False
elif bt in rm_bt:
if active_tab == "Taxa" and bt == rm_bt[1]:
if "Removes all files and taxa" \
not in self.previous_mouse_over:
if not bt.disabled:
create_fancy_label(
"Removes all files and taxa",
rm_bt[1], adjust_pos=True,
c=(1, .33, .33, 1))
elif active_tab == "Files" and bt == rm_bt[0]:
if "Removes all files and taxa" \
not in self.previous_mouse_over:
if not bt.disabled:
create_fancy_label(
"Removes all files and taxa",
rm_bt[0], adjust_pos=True,
c=(1, .33, .33, 1))
elif bt in part_bts:
bt_text = {"data/backgrounds/group_bt.png":
"Merge selected partitions",
"data/backgrounds/split_bt.png":
"Split selected partitions",
"data/backgrounds/add_bt35.png":
"Import partition scheme"}
if active_tab == "Partitions":
create_fancy_label(
bt_text[bt.background_normal],
bt, adjust_pos=True, orientation="vertical")
else:
# If no collision is detected, remove any remaining
# label widget
if collision is False and \
self.old_mouse_over in self.root_window.children:
self.root_window.remove_widget(self.old_mouse_over)
# Only do this when Statistics screen is on
if self.screen.name == "Statistics" and not self.show_side_panel \
and not self._popup in self.root_window.children:
if self._determine_collision(self.screen.ids.sum_stats_bt, mp):
collision = True
create_fancy_label("Show summary statistics",
self.screen.ids.sum_stats_bt,
adjust_pos=True)
# Only do this when plot screen is on
if self.screen.name in self.plot_screens and self._popup not in \
self.root_window.children:
# Get PlotToolbar object
toolbar_wgt = None
try:
toolbar_wgt = [x for x in self.root_window.children
if isinstance(x, OrtoPlotToolbar)][0]
except IndexError:
try:
toolbar_wgt = [x for x in self.root_window.children
if isinstance(x, StatsPlotToolbar)][0]
except IndexError:
toolbar_wgt = None
if toolbar_wgt:
# For headless plot screens a back button is added to
# root_window
if self.screen.name == "plot":
# Get back bt
back_bt = [x for x in self.root_window.children
if isinstance(x, BackButton)][0]
if self._determine_collision(back_bt, mp):
if back_bt.opacity != 1:
Animation(
opacity=1, d=.3, t="out_quart").start(back_bt)
else:
if back_bt.opacity == 1:
Animation(
opacity=.2, d=.3, t="out_quart").start(back_bt)
# Change toolbar opacity to become visible when collision is
# true
if self._determine_collision(toolbar_wgt, mp):
if toolbar_wgt.opacity != 1:
Animation(
opacity=1, d=.3, t="out_quart").start(toolbar_wgt)
else:
if toolbar_wgt.opacity == 1:
Animation(
opacity=.2, d=.3, t="out_quart").start(toolbar_wgt)
if self.screen.name == "Statistics":
try:
toggle_wgt = [x for x in self.root_window.children
if isinstance(x, StatsToggleWgt)][0]
if self._determine_collision(toggle_wgt, mp):
if toggle_wgt.opacity != 1:
Animation(opacity=1, d=.3, t="out_quart"). \
start(toggle_wgt)
else:
if toggle_wgt.opacity == 1:
Animation(opacity=.2, d=.3, t="out_quart"). \
start(toggle_wgt)
except IndexError:
pass
# Check for collision with export figure or export table buttons
if self._determine_collision(toolbar_wgt.ids.export_fig, mp):
collision = True
if "Export as graphics" not in [x.id for x in
self.root_window.children]:
# Create fancy label
create_fancy_label("Export as graphics",
toolbar_wgt.ids.export_fig,
line_c=(0.216, 0.67, 0.784, 1))
elif self._determine_collision(toolbar_wgt.ids.export_table,
mp):
collision = True
if "Export as table" not in [x.id for x in
self.root_window.children]:
# Create fancy label
create_fancy_label("Export as table",
toolbar_wgt.ids.export_table,
line_c=(0.216, 0.67, 0.784, 1))
if self.screen.name != "Statistics":
if self._determine_collision(toolbar_wgt.ids.export_group,
mp):
collision = True
if "Export group" not in [x.id for x in
self.root_window.children]:
# Create fancy label
create_fancy_label("Export group",
toolbar_wgt.ids.export_group,
line_c=(0.216, 0.67, 0.784, 1))
# Only do this in Orthology screen
if (self.screen.name == "Orthology" and self.show_side_panel is
False and self._popup not in self.root_window.children):
id_to_txt = {"sp_vis": "Species focused exploration",
"gn_vis": "Ortholog focused exploration"}
# Determine collision with add groups button
if self._determine_collision(self.screen.ids.add_group, mp):
collision = True
if "Add group files" not in [x.id for x in
self.root_window.children]:
# Create fancy label
create_fancy_label("Add group files",
self.screen.ids.add_group,
adjust_pos=True)
# Determine collision with orthology graph visualization bts
for bt, bt_id in zip([self.screen.ids.sp_vis,
self.screen.ids.gn_vis],
["sp_vis", "gn_vis"]):
if self._determine_collision(bt, mp):
collision = True
if id_to_txt[bt_id] not in [x.id for x in
self.root_window.children]:
# Create fancy label
create_fancy_label(id_to_txt[bt_id], bt,
orientation="vertical",
adjust_pos=True)
if collision is False and self.mouse_over_ready:
self.previous_mouse_over = ""
clear_mouse_overs()
[docs] def switch_path_wgt(self, wgt_id, path_bx, fc_wgt):
def path_updater():
if os.path.exists(txt.text):
fc_wgt.path = unicode(txt.text)
fc_wgt.previous_dir.append(fc_wgt.path)
else:
return self.dialog_floatcheck(
"Directory does not exist", t="error")
label = PathLabel()
txt = PathText(id="path_editor")
fc_wgt.bind(path=txt.setter("text"))
fc_wgt.bind(path=label.setter("text"))
path_bx.clear_widgets()
if wgt_id == "label":
label.text = fc_wgt.path
path_bx.add_widget(label)
else:
txt.text = fc_wgt.path
txt.bind(on_text_validate=lambda x: path_updater())
path_bx.add_widget(txt)
path_bx.children[0].focus = True
[docs] def create_folder(self, text):
path = self._popup.content.ids.sd_filechooser.path
dir_name = join(path, text)
if os.path.exists(dir_name):
return self.dialog_floatcheck(
"The specified folder already exists", t="error")
else:
os.makedirs(dir_name)
self._popup.content.ids.sd_filechooser.path = unicode(dir_name)
self.dismiss_subpopup()
# ######################### SCREEN NAVIGATION ##########################
[docs] def go_screen(self, idx, direct="left"):
"""
Method used to go to a specific screen by specifying and index and
transition direction
:param idx: integer. Index value of the screen from
self.screen_names
:param direct: string. The direction of the transition
"""
if self.screen:
if self.screen.name not in self.plot_screens or \
self.screen.name == "Statistics":
screen_path = join(self.cur_dir, "data", "screens",
"{}.kv".format(self.screen.name))
self.loaded_screens[screen_path] = self.screen
# Automatic removal of plot toolbar when not in a plot screen
if self.screen_names[idx] not in self.plot_screens:
self.dismiss_plot_wgt()
# Automatic removal of fancy dropdown
self.remove_fancy_dropdown()
# Removes old toolbar when switching directly from orto plot
# widget to Statistics
if self.screen_names[idx] == "Statistics" and \
self.current_screen != "Statistics":
self.dismiss_plot_wgt()
self.index = idx
# Precludes a transition if the current screen is the same as the
# target screen
if self.current_screen != self.screen_names[idx]:
# Update previous screen
self.previous_screen = self.current_screen
# Update current screen
self.current_screen = self.screen_names[idx]
# Make the switch
self.root.ids.sm.switch_to(self.load_screen(idx),
direction=direct)
if self.current_screen == "Statistics":
# Generate or update statistics screen
self.statistics_show_summary()
[docs] def go_previous_screen(self):
"""
Method that returns to the previous screen, set by
self.previous_screen
"""
if self.previous_screen != "":
previous_idx = self.screen_names.index(self.previous_screen)
self.go_screen(previous_idx, "right")
[docs] def load_screen(self, idx):
"""
Loads the current screen according to the corresponding kv file
:param idx: The index of the screen to be loaded
"""
if self.loaded_screens[self.available_screens[idx]]:
self.screen = self.loaded_screens[self.available_screens[idx]]
if self.screen.name == "fc":
self.screen.ids.icon_view_tab.selection = []
else:
self.screen = Builder.load_file(self.available_screens[idx])
# If the screen to be loaded is the filechooser, set the
# home path as the default
if basename(self.available_screens[idx]) == "fc.kv":
self.screen.ids.icon_view_tab.path = self.home_path
# Initialize bookmarks
self.bookmark_init(self.screen.ids.sv_book,
self.screen.ids.sv_mycomp,
self.screen.ids.icon_view_tab)
# Disengage acion bar toggle buttons when entering main filechooser
if basename(self.available_screens[idx]) == "fc.kv":
self.disengage_groups()
if basename(self.available_screens[idx]) == "Statistics.kv":
# Add StatsToggleWidget, if present
try:
if isinstance(self.screen.ids.plot_content.children[0].
children[0], Image):
self.show_plot_toolbar(toolbar_type="stats")
if self.previous_stats_toggle:
self.root_window.add_widget(
self.previous_stats_toggle)
except IndexError:
pass
return self.screen
[docs] def go_carousel(self, slide, bt_id):
"""
Method used by other buttons outside the side buttons of the side
panel to go to specific slides of the side panel
:param slide: int, the index of the target slide
:param bt_id: string, the id of the corresponding button
:return:
"""
self.toggle_groups(self.root.ids[bt_id])
self.root.ids[bt_id].state = "down"
panel_car = self.root.ids.carousel
panel_car.load_slide(panel_car.slides[slide])
@staticmethod
[docs] def toggle_groups(wgt):
"""
This method generates a desired behaviour for groups of toggle
buttons By default, when a toggle button is pressed, the state
will be down and a new screen/slide is presented. However,
if the same toggle button is pressed again, it's state will
return to the normal state while the same screen/slide is showed.
To prevent this behaviour, this method will disable the active
toggle button in the group and enable any other previously
disabled button.
To allow a seamless transition, ensure that background_disabled_dow
is the same as background_down, and that disabled_color is the
same as color.
:param wgt: The toggle button widget. Must belong to a group.
"""
# Iterating over the children of the parent may not be optimal, but
# using the get_widgets(groupname) method could result in some
# issues with garbage collector of kivy. So, for now, this will
# iterate over all children of the toggle button's parent
for i in [x for x in wgt.parent.children
if isinstance(x, ToggleButton)]:
if i.disabled:
i.disabled = False
i.state = "normal"
wgt.disabled = True
[docs] def disengage_groups(self):
"""
Methods used to unselect all module header buttons, in case the user
enters the main filechooser
"""
for wgt in [self.root.ids.h_ortho, self.root.ids.h_process,
self.root.ids.h_stat]:
wgt.disabled = False
wgt.state = "normal"
[docs] def check_action(self, text, func, bt_wgt=None, args=None,
popup_level=1, check_wgt=CheckDialog):
"""
General purpose method that pops a dialog checking if the user
wants to perform a certain action. This method should be passed
as a function on the 'on_*' with the final function and original
widget triggering the event as arguments.
By default, the check action dialog is a CheckDialog widget,
but alternative widgets can be used by passing them to the
check_wgt option. If so, properties such as size, separator
color, etc must be specified for such cases.
:param text: string, text to appear in the dialog
:param func: final function if the users chooses to proceed
:param bt_wgt: widget where the initial 'on_' event occurred
:param args: list, of arguments to be passed on to func
:param popup_level: int, level of popup. 1 for _popup, 2 for
_subpopup and 3 for exit popup
:param check_wgt: Widget. Specified the check dialog widget
Usage example:
This can be applied to the removal button of the bookmarks. In this
case, the event of the removal button must be like this:
remove_button.bind(partial(self.check_action,
self.remove_bookmark_bt))
where, self.check_action is this method, and self.remove_bookmark_bt
is the function that will actually remove the bookmark button. This
function is then bound to the "OK" button of the check dialog.
By default, the last argument is the bt_wgt.
"""
if popup_level == 1:
check_content = check_wgt(cancel=self.dismiss_popup)
elif popup_level == 2:
check_content = check_wgt(cancel=self.dismiss_subpopup)
else:
# Do nothing if exit popup already exists. The return True
# prevents the app from closing, since in that stage it will
# be listening to return False to close.
if self._exit_popup in self.root_window.children:
return True
check_content = check_wgt(cancel=self.dismiss_exit)
if isinstance(check_content, CheckDialog):
# Set size
size = (300, 200)
# Sep separator color
sep_color = self._red
# Set popup title
title = "Warning!"
# Set text for dialog
check_content.ids.check_text.text = text
elif isinstance(check_content, CheckProject):
title = "Project overview"
size = (350, 230)
sep_color = self._blue
check_content.ids.proj_name.text = text[0]
check_content.ids.file_num.text = text[1]
# Set the function to be called when dispatching the on_release
# event of the check_ok button.
if bt_wgt and not args:
check_content.ids.check_ok.bind(
on_release=lambda val: func(bt_wgt))
else:
if args:
check_content.ids.check_ok.bind(on_release=lambda val:
func(*args))
else:
check_content.ids.check_ok.bind(
on_release=lambda val: func())
if popup_level in [1, 2]:
self.show_popup(title=title, content=check_content,
size=size, popup_level=popup_level,
separator_color=sep_color)
elif popup_level == 3:
self._exit_popup = CustomPopup(
title="[b]%s[/b]" % title,
content=check_content,
size=size,
size_hint=(None, None),
separator_color=sep_color,
border_color=sep_color,
separator_height=0.6,
title_color=sep_color,
background="data/backgrounds/transparent.png")
self._exit_popup.open()
return True
[docs] def check_file(self, path, file_name, idx, bw=None):
"""
Method used by some filechooser dialogs. Checks whether the provided
file name already exists. If so, issues a check_action popup. If
not, proceeds as normal
:param path: string, complete path
:param file_name: string, file name
:param idx: string, operation identifier
:param bw: bool, argument specific for export_graphic idx.
Determines whether a figure will be exported in original color or
grayscale
"""
# Stores methods. key: idx; first value element, method to apply;
# second value element, list of arguments; third value element the
# file_name extension
methods = {
"main_output":
[self.save_file, [path, file_name, idx], ""],
"export":
[self.export_names, [path, file_name], ".txt"],
"export_table":
[self.export_table, [path, file_name], ".csv"],
"export_outliers":
[self.export_table, [path, file_name, ".txt"], ".txt"],
"export_graphic":
[self.export_graphic, [path, file_name, bw], ""],
"group":
[self.orto_export_groups, ["group", path, file_name], ""]
}
# Check if files exists.
try:
p = join(unicode(path, "utf8"), file_name + methods[idx][2])
except TypeError:
p = join(path, file_name + methods[idx][2])
if os.path.exists(p) and idx != "main_output":
self.check_action(
"The file {} already exists. Overwrite?".format(file_name),
methods[idx][0],
**{"args": methods[idx][1], "popup_level": 2})
else:
methods[idx][0](*methods[idx][1])
# Close popup automatically except for the group idx
if idx != "group":
self.dismiss_popup()
# ########################## GENERAL USE ###############################
[docs] def run_in_background(self, func, second_func, args1, args2=None,
no_arg2=False, msg="Crunching data...",
cancel=True):
"""
This method is intended to run time/resource consuming operations in
the background, without freezing the app, and return the final
result to the main thread. This means that complex methods that
perform multiple changes to the App's attributes are not suitable
for this method (changes in a secondary thread will not change
the App in the main thread). Therefore, the simplest solution to
the problem is to isolate the time consuming parts of those
methods, run them in the background, and get their result. Then,
their result is piped to the follow-up fuction provided as argument
:param func: intensive callable bound method to run in the
background
:param second_func: Follow-up bound method that will use the value
returned by func
:param args1: list, with the arguments for func t method. No
keywords allowed
:param args2: list, with arguments for second_func. These will be
added to the argument list returned by func
:param no_arg2: Boolean. Whether func will return something to
second_func
:param msg: string, message to appear in waiting dialog.
:param cancel: Boolean, Shows a functional cancel button that
interrupts the background process, if True.
"""
def check_process_status(p, second_function, args, man, dt):
"""
This scheduled function will check the status of the second
process. When finished, it will dismiss the waiting popup,
get the value returned by the second process and issue the
following method
:param p: Process object.
:param second_function: function object. If not None, this
function will be executed in the main process, once the first
process is finished.
:param args: list. Contains the argument to be passed at the
second_function
:param man: Manager object.
"""
# Kill switch. Terminating thread by user
if self.terminate_background:
# Issuing the kill order to the child thread. This signal
# will propagate through the worker's methods and graciously
# terminate
shared_ns.stop = True
# This small delay seems to fix manager shutdown issues
time.sleep(.1)
# Shutting down manager that provides communication between
# parent and child thread
man.shutdown()
# Close loading popup dialog
self.dismiss_popup()
# Unschedule the current function
Clock.unschedule(check_func)
# Join child process and exit
p.join()
return
if "img" in self._popup.content.ids:
self._popup.content.ids.img.rotation -= 10
if shared_ns.counter:
self._popup.content.ids.msg2.text = "{}/{}".format(
shared_ns.counter, shared_ns.files)
if not p.is_alive():
try:
if shared_ns.exception:
return self.dialog_floatcheck(
"An unexpected error occurred. Check the app"
" logs", t="error")
except:
pass
try:
val = shared_ns.val
except AttributeError:
val = None
Clock.unschedule(check_func)
self.dismiss_popup()
# Manager must be shutdown before closing the process,
# otherwise the data pipe will prevent the process from
# closing
man.shutdown()
p.join()
# Checks if there is a second function to run and whether
# there are additional arguments for secondary function
if not no_arg2:
if second_func:
if args2 and val:
val.extend(args)
elif args2:
val = args
second_function(*val)
elif second_function:
second_function()
# Set up manager and shared name space to share information
# between background and main processes
manager = multiprocessing.Manager()
shared_ns = manager.Namespace()
shared_ns.counter = 0
# Set kill switch flag
shared_ns.stop = False
# Remove lock from background process
self.terminate_background = False
# Create process
p = threading.Thread(target=background_process,
args=(func, shared_ns, args1))
p.daemon = True
p.start()
# Remove any possible previous popups
self.dismiss_popup()
# Create waiting dialog
content = CrunchData()
# Set label
content.ids.msg.text = msg
# Determine whether widget should have cancel option or not
if cancel:
# Set functionality of cancel button
size = (230, 230)
content.ids.cancel_bt.bind(
on_release=lambda x: setattr(self, "terminate_background",
True))
else:
# Remove cancel button
size = (230, 200)
content.ids.cancel_anc.remove_widget(content.ids.cancel_bt)
content.remove_widget(content.ids.cancel_anc)
# Create popup with waiting dialog
self.show_popup(title="", content=content, size=size,
separator_color=(0, 0, 0, 0),
border_color=tm.c_popup_border,
auto_dissmiss=False)
# Schedule function that checks the process' pulse
check_func = partial(check_process_status, p,
second_func, args2, manager)
Clock.schedule_interval(check_func, .1)
# ###################### BOOKMARKS OPERATIONS ##########################
[docs] def bookmark_init(self, wgt, dev_wgt, fc_wgt, popup_level=1):
"""
This will create a pickle file containing a list with the bookmarks
for the file chooser menu. If no file exists, it will create an
empty one. If a file already exists, it will load the available
bookmarks
:param wgt: Widget object where the bookmark button will be
:param dev_wgt: Widget object where the system bookmarks will be
:param fc_wgt: Filechooser widget associated with the bookmarks
:param popup_level: int, specifies the level of the check_action popup
that appears when attempting to remove a bookmark
"""
if exists(self.bm_file):
self.bookmarks = pickle.load(open(self.bm_file, "rb"))
# Retrieving the bookmark path list from the self.bookmarks
bk_list = self.bookmarks[0]
for bk in bk_list:
self.add_bookmark_bt(bk, wgt, fc_wgt, popup_level=popup_level)
else:
pickle.dump(self.bookmarks, open(self.bm_file, "wb"))
# Trying to import FM bookmarks in ~/.config/gtk-3.0/bookmarks.
# This will happen only the first time the app is executed, when
# the bookmarks will be saved. From there on, bookmarks will be
# managed in the app
if sys.platform in ["linux", "linux2"]:
if exists(join(self.home_path, ".config", "gtk-3.0",
"bookmarks")):
with open(join(self.home_path, ".config", "gtk-3.0",
"bookmarks")) as bk_file:
for bk_line in bk_file:
bk = bk_line.split()[0].replace("file://", "")
# urllib will ensure special characters with
# punctuation are correctly showed
bk = urllib.unquote(bk)
self.save_bookmark(bk, wgt, fc_wgt,
popup_level=popup_level)
# Get main paths for linux
if sys.platform in ["linux", "linux2"]:
# System
self.add_bookmark_bt("/", dev_wgt, fc_wgt, name="System",
rm_bt=False)
# Home
self.add_bookmark_bt(self.home_path, dev_wgt, fc_wgt,
name="Home", rm_bt=False)
# Get removable media
x = subprocess.Popen(["mount | grep -e /media -e /mnt | awk '{"
"print $3}'"], shell=True, stdout=subprocess.PIPE)
removable_media = x.communicate()[0]
if removable_media:
for path in removable_media.split("\n")[:-1]:
# Add path to removable_media attribute. This attribute
# is used when updating removable media real time in
# _check_removable_media
if path not in self.removable_media:
self.removable_media.append(path)
name = basename(path)
self.add_bookmark_bt(path, dev_wgt, fc_wgt, name,
rm_bt=False)
# Get main devices for windows
elif sys.platform in ["win32", "cygwin"]:
devices = re.findall(
r"[A-Z]+:.*$", os.popen("mountvol /").read(), re.MULTILINE)
# Prevent no disk on drive popup error in windows
SEM_FAILCRITICALERRORS = 1
SEM_NOOPENFILEERRORBOX = 0x8000
SEM_FAIL = SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS
kernel32 = ctypes.WinDLL('kernel32')
oldmode = ctypes.c_uint()
kernel32.SetThreadErrorMode(SEM_FAIL, ctypes.byref(oldmode))
for d in devices:
if exists(d):
self.removable_media.append(d)
self.add_bookmark_bt(d, dev_wgt, fc_wgt, rm_bt=False,
name=os.path.splitdrive(d)[0])
# Home directory
self.add_bookmark_bt(self.home_path, dev_wgt, fc_wgt, rm_bt=False)
# Get some favorites for MacOS
elif sys.platform == "darwin":
# Get mounted volumes
for d in os.listdir("/Volumes"):
self.removable_media.append(join("/Volumes", d))
self.add_bookmark_bt(join("/Volumes", d), dev_wgt, fc_wgt,
name=d, rm_bt=False)
# Home
self.add_bookmark_bt(self.home_path, dev_wgt, fc_wgt,
name=basename(self.home_path),
rm_bt=False)
# Desktop
self.add_bookmark_bt(join(self.home_path, "Desktop"), wgt,
fc_wgt, "Desktop")
# Documents
self.add_bookmark_bt(join(self.home_path, "Documents"), wgt,
fc_wgt, "Documents")
# Downloads
self.add_bookmark_bt(join(self.home_path, "Downloads"), wgt,
fc_wgt, "Downloads")
[docs] def save_bookmark(self, path, wgt, fc_wgt, popup_level=1):
"""
This adds functionality to the FileChooser "Add bookmark" button. It
will grab the selected path and add it to a storage list that
will be saved as a pickle object and stored in a file defined in
self.bm_file.
:param path: String containing the path of the bookmark
:param wgt: Widget where the bookmark will be added
:param fc_wgt: FileChooser widget that the bookmark will affect
:param popup_level: int, specifies the level of the check_action popup
that appears when attempting to remove a bookmark
"""
# Load bookmarks object
self.bookmarks = pickle.load(open(self.bm_file, "rb"))
# Check if bookmark already exists. Only add bookmark if it does not
# exist
if path not in self.bookmarks[0]:
# Add bookmarks to the full path list
self.bookmarks[0].append(path)
# Add mapping of the full path to the bookmark name
new_map = {basename(path): path}
self.bookmarks[1] = dict(list(self.bookmarks[1].items()) +
list(new_map.items()))
self.add_bookmark_bt(path, wgt, fc_wgt, popup_level=popup_level)
pickle.dump(self.bookmarks, open(self.bm_file, "wb"))
[docs] def add_bookmark_bt(self, bk, wgt, fc_wgt, name=None, rm_bt=True,
popup_level=1):
"""
This will add a bookmark button, along with its removal button. Only
a bookmark path will be necessary.
The path of the bookmark will be associated to the respective button
by it's id.
:param bk: string. bookmark file path
:param wgt: Widget, preferentially a gridlayout where the bookmark
buttons will be added
:param fc_wgt: FileChooser widget in which bookmark operations will
be performed
:param name: string, optional name for bookmark button instead of
the basename of the path
:param rm_bt: Boolean, If True, a removal button will be added with
the bookmark, else the removal button will not be added. The latter
case is used for System devices bookmarks.
:param popup_level: int, specifies the level of the check_action popup
that appears when attempting to remove a bookmark
"""
# Ignore if path does not exist / is not a directory
if not os.path.isdir(bk):
return
bookmark_name = basename(bk)
# Define bookmark button
bt = TFButton(text=name if name else bookmark_name, id=bk,
bold=True, height=30, size_hint=(.8, None),
background_normal=join("data", "backgrounds",
"bt_process.png"),
background_down=join("data", "backgrounds",
"bt_process_off.png"))
# Bind to function that loads bookmark path into filechooser
bt.bind(on_release=lambda x: self.bookmark_load(x, fc_wgt))
# Add to list for mouse over purposes
self.bookmarks_bt.append(bt)
wgt.add_widget(bt)
if rm_bt:
# Define bookmark removal button
xbt = Button(size_hint=(None, None), width=30,
height=30, id="%sX" % bk, border=(0, 0, 0, 0),
background_normal=join("data", "backgrounds",
"remove_bt.png"),
background_down=join("data", "backgrounds",
"remove_bt_down.png"))
# Bind to function that removes bookmark button as well as the
# path from self.bm_file
xbt.bind(on_release=partial(self.check_action,
"Are you sure you want to remove"
" this bookmark?",
self.remove_bookmark_bt,
popup_level=popup_level))
wgt.add_widget(xbt)
[docs] def bookmark_load(self, value, wgt):
"""
Provided a bookmark button object, it loads the bookmark file path
that is stored in the button id.
:param value: bookmark button object
:param wgt: Filechooser widget that will show the bookmark
"""
path = value.id
if os.path.exists(path):
try:
wgt.previous_dir.append(wgt.path)
except KeyError:
pass
wgt.path = unicode(path)
wgt.selection = []
else:
self.dialog_floatcheck(
"The path to the selected bookmark no longer exists.",
t="error")
[docs] def remove_bookmark_bt(self, value):
"""
Adds functionality to the removal button associated with each
bookmark button. This will not only remove the
:param value: The removal button widget
"""
# Get the widget to remove the button
parent_obj = value.parent
# Get the bookmark button, using the removal button id
bk_idx = value.id[:-1]
bk_bt = [x for x in parent_obj.children if bk_idx == x.id][0]
# Remove both bookmark and removal buttons
parent_obj.remove_widget(value)
parent_obj.remove_widget(bk_bt)
# Update gridlayout height for scrolling purposes
parent_obj.height -= self.root.height * 0.07
# Core changes
bk_name = basename(bk_idx)
# Remove bookmark path from list and mapping dictionary
self.bookmarks[0].remove(bk_idx)
del self.bookmarks[1][bk_name]
# Update self.bm_file
pickle.dump(self.bookmarks, open(self.bm_file, "wb"))
# ####################### PLOT SCREEN OPERATIONS #######################
[docs] def dialog_export_graphic(self):
"""
Creates a filechooser dialog for graphics exportation. It differs
from other filechooser dialogs in the presence of a spinner to
select the graphical extension
:return:
"""
content = ExportGraphics(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
self.show_popup(title="Export as graphic...", content=content,
size_hint=(.9, .9))
[docs] def export_graphic(self, path, file_name, bw):
"""
Saves the current plot object into a file based on file name and
extension
:param path: string, path to final directory
:param file_name: string, name of graphic file
:param bw: bool, Determined whether the graphic will be in the
original colors (False) or grayscale (True)
"""
temp_f = None
f_path = join(path, file_name)
if bw:
temp_f = join(self.temp_dir, file_name)
file_name = os.path.splitext(file_name)[0] + "_bw" + \
os.path.splitext(file_name)[1]
f_path = join(path, file_name)
if self.current_lgd:
self.current_plot.savefig(temp_f if temp_f else f_path,
bbox_extra_artists=(self.current_lgd,),
bbox_inches="tight")
else:
self.current_plot.savefig(temp_f if temp_f else f_path,
bbox_inches="tight")
if bw:
image = misc.imread(temp_f, flatten=True)
misc.imsave(f_path, image)
os.remove(temp_f)
self.dialog_floatcheck("Graphic successfully exported!", t="info")
[docs] def export_table(self, path, file_name, ext=".csv", warning_dlg=True):
"""
Saves the current_table list attribute to a .csv file.
:param path: string, path to final directory
:param file_name: string, name of table file
:param warning_dlg: boolean. If True, issue a float dialog when
the operation is done. If false do not issue the float dialog.
"""
# Create table file object handle
table_handle = open(join(path, file_name + ext), "w")
# Writing table. Each entry in self.current_table should represent a
# line in the table
for l in self.current_table:
table_handle.write(";".join([str(x) for x in l]) + "\n")
# Close file object
table_handle.close()
if warning_dlg:
self.dialog_floatcheck("Table successfully exported!", t="info")
# ####################### SIDE PANEL OPERATIONS ########################
[docs] def dialog_about(self):
"""
Dialog with the about information on TriFusion
"""
content = AboutDialog()
content.version = __version__
content.build = __build__
self.show_popup(title="About TriFusion", content=content,
close_bt=True, size=(480, 410))
[docs] def sidepanel_on_touch(self, touch):
"""
This function is binded to the app Window so that it can handle any
touch_up events. Once the side panel is open, this allows any mouse
click outside the panel to close it. It gathers information on the
mouse and side panel position and evaluates a collision. It will
trigger the side panel closing only when four conditions are met:
- When there is a mouse input outside the side panel
- When the variable controlling the side panel (show_side_panel) is
True, meaning that the panel is extended
- When the mouse input is outside the previous button in the action
bar, which is also used to toggle the side panel. This prevents
issues of toggling the side panel twice with one mouse input
- When a popup is not open. There are several buttons in the side
bar that open whose position is outside the side bar. The user
should be
able to click anywhere in the popup without the side panel closing.
In addition, it will handle the status of the partition box dialog,
associated with the sidepanel. While this box is active, the
sidepanel must remain open. However, clicks outside the partition
box will close it.
:param touch: Touch event
"""
# Set touch as a app attribute
self.touch = touch
def animate_sidebar():
# ANIMATIONS with hierarchy
# Animation of main BoxLayout containing child ScrollViews
self.sidepanel_animation(width=0, wgt=self.root.ids.main_box)
# Animation of both scrollviews
self.sidepanel_animation(width=0, wgt=self.root.ids.sp)
self.sidepanel_animation(width=0, wgt=self.root.ids.sp_bts)
self.show_side_panel = not self.show_side_panel
# Get mouse position
mp = self.root_window.mouse_pos
# Get side panel and previous button widgets
side_panel_wgt = self.root.ids.main_box
ap = self.root.ids.ap
# If sidepanel's more options widget is active, check if touch is
# outside. If so, remove widget
if self.sp_moreopts:
wgt = [x for x in self.root_window.children if
isinstance(x, SP_MoreOpts_Dialog)][0]
if not wgt.collide_point(mp[0], mp[1]) and not \
self._determine_collision(self.root.ids.file_opt, mp):
self.sidepanel_remove_moreopts()
# If fancy dropdown is dropped, check if touch is oustide. If so,
# remove the widget
if self.fancy_dropped:
wgt = [x for x in self.root_window.children if
isinstance(x, FancyDropDown)][0]
if not self._determine_collision(wgt, mp):
self.remove_fancy_dropdown()
# Check for existence of a partition dialog box
partition_box = [x for x in self.root_window.children if
isinstance(x, PartitionsDialog)]
# If the partition box exists and the collision is outside it
if partition_box and not partition_box[0].collide_point(
mp[0], mp[1]):
# Check if spinner is open
spin1 = partition_box[0].ids.codon_spin.is_open
spin2 = [x.is_open for x in
partition_box[0].ids.model_bx.children]
# If the spinners are not open, remove
if True not in spin2 and not spin1:
rm_bt = [x for x in self.root_window.children if
isinstance(x, RemoveFloat)][0]
self.root_window.remove_widget(partition_box[0])
self.root_window.remove_widget(rm_bt)
# Check for conditions to close the side panel.
# If touch is out of panel; if panel is open; is touch is out of
# menu button; a popup is not open
if side_panel_wgt.collide_point(mp[0], mp[1]) is False \
and self.show_side_panel \
and ap.collide_point(mp[0], mp[1]) is False \
and self._popup not in self.root_window.children \
and not partition_box \
and not self.sp_moreopts:
if self.screen.name == "Process" or \
self.screen.name == "Orthology":
queue_bt = self.screen.ids.queue_bt
if queue_bt.collide_point(mp[0], mp[1]) is False:
animate_sidebar()
else:
animate_sidebar()
[docs] def toggle_sidepanel(self):
"""
Method controlling the animation toggling of the side panel
"""
if self.lock_side_panel:
return
# Closes partition box, if open
self.remove_partition_box()
# Do not toggle when the following conditions are met
if self.screen.name == "fc":
if self.screen.ids.text_filter.focus:
return
if isinstance(self.screen.ids.path_bx.children[0], PathText):
if self.screen.ids.path_bx.children[0].focus:
return
# Toggling the state of the panel. This attribute is the main
# controller of the side panel state. When its True, the side panel
# is extended, otherwise the side panel is hidden
self.show_side_panel = not self.show_side_panel
# Lock the sidepanel animation. It will only be released after the
# animations are over
self.lock_side_panel = True
Clock.schedule_once(lambda x: setattr(self, "lock_side_panel", False),
.4)
if self.show_side_panel:
# Redraw the side panel layout. This will ensure that the widget
# is always on top of all widgets.
self.root.ids.bx1.remove_widget(self.root.ids.panel_float)
self.root.ids.bx1.add_widget(self.root.ids.panel_float)
# Fixing the width of the side panel
# Main panel width
sv_panel_width = 330
# Side buttons width
sv_bts_width = 60
else:
sv_panel_width, sv_bts_width = 0, 0
# ANIMATIONS with hierarchy
# Animation of main BoxLayout containing child ScrollViews
self.sidepanel_animation(width=sv_panel_width * 1.2,
wgt=self.root.ids.main_box)
# Animation of both scrollviews
self.sidepanel_animation(width=sv_panel_width,
wgt=self.root.ids.sp)
self.sidepanel_animation(width=sv_bts_width,
wgt=self.root.ids.sp_bts)
@staticmethod
[docs] def sidepanel_animation(width, wgt):
Animation(width=width, d=.3, t="out_quart").start(wgt)
[docs] def sidepanel_search_bts(self, txt, panel):
"""
Performs a search for file or taxa buttons in the side panel based
on the text string provided
:param txt: string, the expression used for the search
:param panel: string, the panel to perform the search. Can be either
'files' or 'taxa'.
"""
if panel == "files":
# Setting which original button list
bt_list = self.file_list
# Setting which sink grid layout
gl_wgt = self.root.ids.file_sl
create_bts_mtd = self.sidepanel_create_bts
elif panel == "taxa":
bt_list = sorted(self.alignment_list.taxa_names)
gl_wgt = self.root.ids.taxa_sl
create_bts_mtd = self.sidepanel_create_bts
else:
bt_list = list(self.alignment_list.partitions.partitions.keys())
gl_wgt = self.root.ids.partition_sl
create_bts_mtd = self.sidepanel_create_part_bts
# Find buttons that match the txt string
if panel == "files":
found_bts = [basename(el) for el in bt_list if
txt.lower() in basename(el).lower()]
else:
found_bts = [el for el in bt_list if txt.lower() in el.lower()]
# Clear the grid and populate with the found bts
gl_wgt.clear_widgets()
mouse_bts = []
for txt in found_bts:
if panel == "partitions":
# Get number of alignment for partition
fls = self.alignment_list.partitions.\
partitions_alignments[txt]
bt, inf_bt, x_bt = create_bts_mtd([txt, fls])
else:
bt, inf_bt, x_bt = create_bts_mtd(txt)
gl_wgt.add_widget(bt)
gl_wgt.add_widget(inf_bt)
gl_wgt.add_widget(x_bt)
mouse_bts.append(bt)
if panel == "files":
self.mouse_over_bts["Files"] = mouse_bts
elif panel == "taxa":
self.mouse_over_bts["Taxa"] = mouse_bts
else:
self.mouse_over_bts["Partitions"] = mouse_bts
[docs] def sidepanel_clear_search(self, panel):
"""
Clears previous search string and populates with the original
buttons
:param panel: string, the panel to clear the search. Can be either
'files' or 'taxa'
"""
if panel == "files":
# Setting which original button list
bt_list = self.sp_file_bts
# Setting which sink grid layout
gl_wgt = self.root.ids.file_sl
elif panel == "taxa":
bt_list = self.sp_taxa_bts
gl_wgt = self.root.ids.taxa_sl
else:
bt_list = self.sp_partition_bts
gl_wgt = self.root.ids.partition_sl
# Clear the grid and populate with the found bts
gl_wgt.clear_widgets()
mouse_bts = []
for bt, inf_bt, rm_bt in bt_list:
# Update button states during search operation
# Default states is down for Taxa/Files and normal for
# Partitions
state = "down" if gl_wgt != self.root.ids.partition_sl else \
"normal"
# For files
if bt.id in self.filename_map:
if self.filename_map[bt.id] not in self.active_file_list:
state = "normal"
# For taxa
elif bt.text not in self.active_taxa_list:
state = "normal"
bt.state = state
gl_wgt.add_widget(bt)
gl_wgt.add_widget(inf_bt)
gl_wgt.add_widget(rm_bt)
mouse_bts.append(bt)
if panel == "files":
self.mouse_over_bts["Files"] = mouse_bts
elif panel == "taxa":
self.mouse_over_bts["Taxa"] = mouse_bts
else:
self.mouse_over_bts["Partitions"] = mouse_bts
if panel != "taxa":
try:
if self.file_list[self.count_files + 1]:
gl_wgt.add_widget(LoadMoreBt())
except IndexError:
return
[docs] def sidepanel_remove_moreopts(self):
"""
Removes widgets from the more options dialog
"""
for wgt in [x for x in self.root_window.children if
isinstance(x, SP_MoreOpts_Dialog) or
isinstance(x, FancyMarkerPersist)]:
self.root_window.remove_widget(wgt)
self.sp_moreopts = False
[docs] def remove_fancy_dropdown(self):
"""
Removes FancyDropDown widgets from the root window
:return:
"""
for wgt in [x for x in self.root_window.children if
isinstance(x, FancyDropDown) or
isinstance(x, FancyMarkerPersist)]:
self.root_window.remove_widget(wgt)
self.fancy_dropped = False
[docs] def toggle_fancy_dropdown(self, bt, values, orientation="down",
add_ds_type=None):
"""
Adds functionality to the File/Taxa dropdown menus using a
FancyDropDown widget
:param Button bt: Button instance that was pressed
:param list values: List of values that will populate the dropdown
menu
:param str orientation: Whether the dropdown will go 'down' or 'up'
:param str add_ds_type: If not None, it will provide the additional
data set buttons of the appropriate ds_type ('files' or 'taxa')
"""
def set_ds(cur_bt, ds_name, wgt):
cur_bt.text = ds_name
self.remove_fancy_dropdown()
if self.fancy_dropped:
return self.remove_fancy_dropdown()
# Get vertical size from the number of entries in values
ds_len = len(values)
# If the add_ds_type option is provided, add two more items that will
# correspond to the "active" and "total" datasets
if add_ds_type:
ds_len += 2
# Since bt may be inside a scrollview, this retrieves the real position
# of the widget
real_pos = bt.to_window(bt.pos[0], bt.pos[1])
width = 200
height = ds_len * 35 + 19
# Get position
wgt_x = real_pos[0] - (width - bt.width) / 2
if orientation == "down":
wgt_y = real_pos[1] - 10 - height
else:
wgt_y = real_pos[1] + bt.height + 17
# Correct x position, if outside screen
if wgt_x + width > self.root.width:
# Determine the width that is over the window bound
offset = (wgt_x + width) - self.root.width
wgt_x -= (offset + 20)
# Correct x position to the left
wgt_x = wgt_x if wgt_x >= 20 else 20
# Correct height and y position, if widget falls outside screen
if wgt_y <= 0:
height += (wgt_y - 20)
wgt_y = 20
elif wgt_y + height >= self.root.height:
off_set = (wgt_y + height) - self.root.height
height -= (off_set + 10)
# Get final size
size = [width, height]
# Generate fancy marker
bg = join("data", "backgrounds", "box_arrow_up.png") if \
orientation == "down" else \
join("data", "backgrounds", "box_arrow_down.png")
if orientation == "down":
arrow_pos = (real_pos[0] + (bt.width / 2), real_pos[1] - 10)
else:
arrow_pos = (real_pos[0] + (bt.width / 2),
real_pos[1] + bt.height + 10)
point_wgt = FancyMarkerPersist(
background_normal=bg, pos=arrow_pos, size=(12, 7),
background_color=(0.216, 0.67, 0.784, 1))
# Generate DropDown
dropdown = FancyDropDown(pos=(wgt_x, wgt_y), size=size)
if add_ds_type:
for name_tpl in ["All {}", "Active {}"]:
name = name_tpl.format(add_ds_type)
bg = "data/backgrounds/transparent.png" if name != bt.text \
else "data/backgrounds/bt_process.png"
ds_bt = TFButton(size_hint_y=None, height=30,
text=name,
background_normal=bg)
ds_bt.bind(on_release=partial(set_ds, bt, name))
dropdown.ids.grid_wgt.add_widget(ds_bt)
# Create Buttons for each custom data set group
for name in values:
bg = "data/backgrounds/transparent.png" if name != bt.text else \
"data/backgrounds/bt_process.png"
ds_bt = TFButton(size_hint_y=None, height=30, text=name,
background_normal=bg)
ds_bt.bind(on_release=partial(set_ds, bt, name))
dropdown.ids.grid_wgt.add_widget(ds_bt)
self.root_window.add_widget(point_wgt)
self.root_window.add_widget(dropdown)
self.fancy_dropped = True
[docs] def sidepanel_moreopts_dialog(self, bt):
"""
"""
if self.sp_moreopts:
return self.sidepanel_remove_moreopts()
# Get active tab
active_tab = self.root.ids.main_tp.current_tab.text
# Get position
wgt_x = bt.x + (bt.width * 2) - 135
wgt_y = bt.y + bt.height + 12
# Generate fancy marker
point_wgt = FancyMarkerPersist(
background_normal=join("data", "backgrounds",
"box_arrow_down.png"),
pos=(bt.x + (bt.width * 2), (bt.y + bt.height + 5)),
size=(12, 7), background_color=(0.216, 0.67, 0.784, 1))
dlg_wgt = SP_MoreOpts_Dialog(ds_type=active_tab, pos=(wgt_x, wgt_y))
self.root_window.add_widget(dlg_wgt)
self.root_window.add_widget(point_wgt)
self.sp_moreopts = True
[docs] def load_proteomes(self, selection):
"""
Similar to load method, but specific for loading proteome files.
Given the potential size of these files, they are not stored in
memory, but instead are processed on the fly
:param selection: list. Contains complete paths to the proteome
files
"""
# Check if alignment files have already been loaded. If so, issue
# warning, since the two data types cannot be loaded in simultaneous
if self.file_list:
return self.dialog_warning("Cannot mix alignment and proteome "
"data",
"TriFusion currently has alignment data"
" loaded. If you want to load proteome"
" data, please remove the current"
" data set.")
# Collect input files. Search directories, if they are provided
original_list = []
for f in selection:
if os.path.isdir(f):
original_list.extend([join(f, x) for x in os.listdir(f)
if os.path.isfile(join(f, x))])
else:
original_list.append(f)
# Check if files exist
file_list = [f for f in original_list if exists(f)]
# If input is empty, raise warning and return
if not file_list:
return self.dialog_floatcheck("The provided proteome files do"
" not exist.", t="error")
# Stores invalid proteome files
bad_proteomes = {"invalid": [], "no_fasta": [], "no_protein": []}
good_proteomes = []
# Filter non
for f in file_list:
b = Base()
er = b.autofinder(f)
f_short = basename(f)
if isinstance(er, Exception):
bad_proteomes["invalid"].append(f_short)
elif er[0] != "fasta":
bad_proteomes["no_fasta"].append(f_short)
elif er[1][0] != "Protein":
bad_proteomes["no_protein"].append(f_short)
else:
good_proteomes.append(f)
# If there are proteome files already loaded, extend
if self.proteome_files:
for f in good_proteomes:
if f not in self.proteome_files:
self.proteome_files.append(f)
self.active_proteome_files.append(f)
# If there are no proteome files loaded
else:
self.proteome_files = good_proteomes
self.active_proteome_files = deepcopy(self.proteome_files)
# Update gene filter value to number of proteomes. This will
# automatically set the minimum number of species to the number of
# proteome files, each of which should represent a species.
self.orto_min_sp = len(self.proteome_files)
# Issue float dialog informing that files have been loaded
if good_proteomes:
self.dialog_floatcheck("%s file(s) successfully loaded" %
len(good_proteomes), t="info")
if set(original_list) - set(file_list):
missing = len(set(original_list) - set(file_list))
self.dialog_floatcheck("%s file(s) could not be found" %
missing, t="warning")
# Update the filename - path mapping attribute
self.filename_map = dict(list(self.filename_map.items()) +
list((x, y) for x, y in
zip([basename(x) for x in
good_proteomes],
good_proteomes)))
# Populate file buttons in side panel
self.original_file_inf = self.get_file_information(
mode="proteome")
self.populate_input_files(mode="proteome")
# Automatically set the default orthology directory as the same
# directory of the input files
# Remove potential trailing dir separators that prevent
# basename from finding the last dir name
self.ortho_dir = self._common_path(file_list).rstrip(sep)
if list(bad_proteomes.values()) != [[], [], []]:
msg = ""
if bad_proteomes["invalid"]:
msg += "The following files are in invalid format:\n%s" \
"\n\n" % ", ".join(bad_proteomes["invalid"])
if bad_proteomes["no_fasta"]:
msg += "The following files are not in FASTA format:\n%s" \
"\n\n" % ", ".join(bad_proteomes["no_fasta"])
if bad_proteomes["no_protein"]:
msg += "The following files do not contain protein " \
"sequences:\n%s\n\n" \
% ", ".join(bad_proteomes["no_protein"])
return self.dialog_warning("Invalid proteome files detected",
msg)
[docs] def update_tabs(self):
"""
Wrapper that updates the contents of the files and taxa tabs
"""
self.populate_input_files()
self.populate_species()
self.populate_partitions()
[docs] def update_taxa(self):
"""
This checks whether some taxa that were specific to some file(s)
were removed when that file is removed.
"""
# If taxa were removed during the update, remove those buttons too
current_tx = [x[0].text for x in self.sp_taxa_bts]
removed_taxa = list(set(current_tx) - set(
self.alignment_list.taxa_names))
if removed_taxa:
for i in removed_taxa:
# Get the corresponding buttons:
x_but_txt = "%sX" % i
bt_obj = [x for x in self.root.ids.taxa_sl.children
if x_but_txt == x.id][0]
self.remove_bt(bt_obj)
try:
self.active_taxa_list.remove(i)
except ValueError:
pass
#self.active_taxa_list = self.alignment_list.taxa_names
[docs] def update_partitions(self):
"""
Updates partition buttons following any change to input data
"""
# Check for missing partitions based on id and remove them
for bt, _, x_bt in self.sp_partition_bts:
if bt.id not in self.alignment_list.partitions.partitions:
self.remove_bt(x_bt, parent_wgt=self.root.ids.partition_sl)
[docs] def update_partition_label(self):
"""
Setsand updates a label on the Partitions tab of the side panel,
informing how partitions files are selected out of the total
partitions
"""
if self.alignment_list:
# Get total number of partitions
total_parts = len(self.alignment_list.partitions.partitions)
# Get selected partitions
active_parts = len([x for x in
self.root.ids.partition_sl.children if
isinstance(x, TGToggleButton) and x.state == "down"])
self.root.ids.partition_lab.text = "%s of %s partitions " \
"selected" % (active_parts, total_parts)
[docs] def update_file_label(self):
"""
Sets and updates a label on the Files tab of the side panel,
informing how many files are selected out of the total files
"""
# Determine which list is used to populate
lst = self.file_list if self.file_list else self.proteome_files
active_lst = self.active_file_list if self.active_file_list else \
self.active_proteome_files
self.root.ids.file_lab.text = "%s of %s files selected" % (
len(active_lst), len(lst))
[docs] def update_sp_label(self):
"""
Sets and updates a label on the Taxa tab of the side panel,
informing how many taxa are selected out of the total taxa. If
the taxa list is empty, it disables the select/deselect buttons
"""
self.root.ids.sp_lab.text = "%s of %s taxa selected" % (
len(self.active_taxa_list), len(self.alignment_list.taxa_names))
# Add disabled no taxa button when empty
if len(self.active_taxa_list) == 0 and \
len(self.alignment_list.taxa_names) == 0:
if "species_temp" not in [x.id for x in
self.root.ids.taxa_sl.children]:
no_bt = Button(id="species_temp", text="No species loaded",
size_hint_y=None, height=40, disabled=True)
self.root.ids["species_temp"] = no_bt
self.root.ids.taxa_sl.add_widget(no_bt)
[docs] def sidepanel_create_bts(self, idx):
# Determine state based on active_file_list
if self.filename_map:
# For files
if idx in self.filename_map:
if self.file_list:
state = "down" if self.filename_map[idx] in \
self.active_file_list else "normal"
elif self.proteome_files:
state = "down" if self.filename_map[idx] in \
self.active_proteome_files else "normal"
# For taxa
else:
state = "down" if idx in self.active_taxa_list else "normal"
else:
state = "down"
bt = TGToggleButton(text=idx, id=idx, state=state, height=30,
size_hint_x=.8, shorten=True,
shorten_from="right")
# Setting horizontal text size for shortening
bt.text_size[0] = bt.size[0] * 2
# Binding functionality to toggle button
bt.bind(on_release=self.toggle_selection)
# Set Information button and add the widget
inf_bt = Button(size_hint=(None, None), width=30,
height=30, id="%s?" % idx,
border=(0, 0, 0, 0),
background_normal=join("data", "backgrounds",
"info_bt_down.png"),
background_down=join("data", "backgrounds",
"info_bt.png"))
inf_bt.bind(on_release=self.get_popup_info)
# Set remove button with event binded and add the widget
x_bt = Button(size_hint=(None, None), width=30,
height=30, id="%sX" % idx,
border=(0, 0, 0, 0),
background_normal=join("data", "backgrounds",
"remove_bt.png"),
background_down=join("data", "backgrounds",
"remove_bt_down.png"))
x_bt.bind(on_release=partial(self.check_action,
"Are you sure you want to remove"
" this item?",
self.remove_bt))
return bt, inf_bt, x_bt
[docs] def sidepanel_add_bts(self, idx, tab_name, partition_fls=None):
"""
Wrapper that adds buttons to the corresponding File, Taxa or
Partitions panel. Also prevents duplicates from being entered and
adds buttons for mouse over events.
:param idx: string. Identifier of the current item. It will be the
name of the button
:param tab_name: string. Identifies in which side panel gridlayout
the button will be added
:param partition_fls: string. Only for Partitions tab_name. Includes
full path of the alignment for the partition.
"""
# Set attributes to be added
if tab_name == "Files":
grid_wgt = self.root.ids.file_sl
bt_list = self.sp_file_bts
elif tab_name == "Taxa":
grid_wgt = self.root.ids.taxa_sl
bt_list = self.sp_taxa_bts
else:
grid_wgt = self.root.ids.partition_sl
bt_list = self.sp_partition_bts
# This prevents duplicate entries from being added
if idx not in [x.id for x in grid_wgt.children]:
# Create buttons
if tab_name == "Partitions":
bt, inf_bt, x_bt = self.sidepanel_create_part_bts(
[idx, partition_fls])
else:
bt, inf_bt, x_bt = self.sidepanel_create_bts(idx)
# Add button to storage for mouse over events
self.mouse_over_bts[tab_name].append(bt)
# Adds buttons to gridlayout
grid_wgt.add_widget(bt)
grid_wgt.add_widget(inf_bt)
grid_wgt.add_widget(x_bt)
# Add all three buttons to the storage attribute
bt_list.append((bt, inf_bt, x_bt))
[docs] def sidepanel_load_more_filebts(self):
max_buttons = self.MAX_FILE_BUTTON + self.count_files
self.root.ids.file_sl.remove_widget(
self.root.ids.file_sl.children[0])
for _ in range(self.count_files, max_buttons):
self.count_files += 1
try:
infile = basename(self.file_list[self.count_files])
self.sidepanel_add_bts(infile, "Files")
except IndexError:
return
self.root.ids.file_sl.add_widget(LoadMoreBt())
[docs] def populate_species(self):
"""
This method grabs the taxa names from the input files that were
selected in the FileChooser widget and populates the Taxa tab in the
main side panel with toggle and remove buttons for each taxon
"""
# Remove the initial disabled button if it's still there
if "species_temp" in self.root.ids.keys():
self.root.ids.taxa_sl.remove_widget(self.root.ids.species_temp)
del self.root.ids["species_temp"]
self.root.ids.taxa_sl.height = 5
# Add a label at the end of the taxa list informing how many taxa
# are currently selected out of the total taxa
self.update_sp_label()
for tx in sorted(self.active_taxa_list):
# Prevents duplicate taxa from being entered
if tx not in [x.id for x in self.root.ids.taxa_sl.children]:
self.sidepanel_add_bts(tx, "Taxa")
[docs] def repopulate_partitions(self):
"""
Wrapper method that re-populates partitions after changes
"""
# Clear partitions
self.root.ids.partition_sl.clear_widgets()
self.count_partitions = 0
# Re-populate partitions
self.populate_partitions()
[docs] def populate_partitions(self):
"""
Populates the partitions tab in the side bar from the partitions
object associated with alignment objects.
This method is used when input files are loaded into the program,
which means there will be no issue with multiple files being
associated with the same partitions. This kind of change is done
a posteriori when importing partition files or setting the
partitions manually.
"""
# Remove initial disabled button, if it's still there
if "partition_temp" in self.root.ids.keys():
try:
self.root.ids.partition_sl.remove_widget(
self.root.ids.partition_temp)
del self.root.ids["partition_temp"]
except ReferenceError:
pass
for partition, fls in self.alignment_list.partitions.iter_files():
if self.count_partitions <= self.MAX_PARTITION_BUTTON:
self.count_partitions += 1
# Create partition buttons
self.sidepanel_add_bts(partition, "Partitions",
partition_fls=fls)
else:
self.root.ids.partition_sl.add_widget(LoadMoreBt())
return
[docs] def partitions_merge_dialog(self):
"""
Dialog that appears when clicking merge partitions asking for the
name of the new partition
"""
content = InputTextDialog(cancel=self.dismiss_popup,
action=lambda x: self.partitions_merge(x))
self.show_popup(title="Choose name for new partition",
content=content,
size=(300, 153.5))
[docs] def partitions_merge(self, name):
"""
Merge active partitions
:param name: string. Name of the new partition
"""
if name in self.alignment_list.partitions.partitions:
return self.dialog_floatcheck(
"A partition named %s already exists." % name,
t="error")
self.alignment_list.partitions.merge_partitions(
self.active_partitions, name)
# Resets active partitions
self.active_partitions = []
# Close popup
self.dismiss_popup()
self.repopulate_partitions()
[docs] def partitions_split(self, new_range=None, new_names=None):
"""
Split an active partition
:param new_range: Optional. Tuple containing the new ranges for the
new partitions
:param new_names: Optional. Tuple containing the new names for each
new partition
"""
# Check whether any of the new partition names already exist. Issue
# an error if so.
if new_names:
if [x for x in new_names if x in
self.alignment_list.partitions.partitions]:
return self.dialog_floatcheck("Duplicate partition names "
"deteced: {}".format(
" ".join([x for x in new_names if x if
self.alignment_list.partitions.partitions])), t="error")
# Get active partition
active_partition = [x.text for x in
self.root.ids.partition_sl.children
if isinstance(x, ToggleButton) and
x.state == "down"][0]
if new_range:
self.alignment_list.partitions.split_partition(
active_partition, new_range, new_names)
else:
self.alignment_list.partitions.split_partition(active_partition)
# Resets ative partitions
self.active_partitions = []
# Close popup
self.dismiss_popup()
self.repopulate_partitions()
[docs] def dialog_partitions_split(self):
content = SplitPartitions(cancel=self.dismiss_popup)
# Get active partition
active_partition = [x.text for x in
self.root.ids.partition_sl.children
if isinstance(x, ToggleButton) and
x.state == "down"][0]
# Get partition range
part_range = self.alignment_list.partitions.\
partitions[active_partition]
# Disable manual split if the current partition has a fragmented
# range
if not isinstance(part_range[0], tuple):
for wgt in [content.ids.ok_bt, content.ids.part1,
content.ids.part2]:
wgt.disabled = True
content.ids.manual_slider.max = 0
else:
content.ids.manual_slider.min = int(part_range[0][0]) + 1
content.ids.manual_slider.max = int(part_range[0][1]) + 1
# If partition contains only one file, disable automatic split by
# files
if len(self.alignment_list.partitions.partitions_alignments[
active_partition]) == 1:
content.ids.auto_split_bt.disabled = True
self.show_popup(title="Split partition", content=content,
size=(420, 320))
if not isinstance(part_range[0], tuple):
self.dialog_floatcheck(
"Manual split in unavailable for partitions with "
"fragmented ranges.",
t="warning")
[docs] def partitions_change_name(self, partition_name, new_name):
"""
Changes name of a partition
:param partition_name: string, Original partition name
:param new_name: string, new partition name
:return:
"""
# Change partition name
self.alignment_list.partitions.change_name(partition_name, new_name)
# Update button in side panel
bt = [x for x in self.root.ids.partition_sl.children
if isinstance(x, ToggleButton) and
x.text == partition_name][0]
bt.text = new_name
[docs] def partitions_import_scheme(self, partition_file):
"""
Imports partitions in partition_file and applies to the
alignment_list partition object. It applies to the entire
alignment_list for now.
:param partition_file: string, path to partition file
"""
self.alignment_list.partitions.read_from_file(partition_file)
# Clear partitions
self.root.ids.partition_sl.clear_widgets()
#C Clear partition button storage
self.sp_partition_bts = []
# Re-populate partitions
self.populate_partitions()
# Update partitions label
self.update_partition_label()
self.dismiss_popup()
[docs] def sidepanel_create_part_bts(self, idx):
"""
Creates buttons for each partition
:param idx: string. unique identifier of partition
"""
part_name = idx[0]
fl_num = str(len(idx[1]))
# Create main button
bt = TGToggleButton(text=part_name, state="normal", id=part_name,
size_hint_y=.8, shorten=True, height=30,
shorten_from="right")
bt.bind(on_release=self.toggle_selection)
# Setting horizontal text size for shortening
bt.text_size[0] = bt.size[0] * 2
# Create file counter button. This button will display the number
# of alignments included in this partition as its text. The
# on_release event will show a popup with a list of the alignment
# files contained
c_bt = Button(size_hint=(None, None), width=30, text=fl_num,
height=30, id="%sC" % part_name, bold=True,
border=(0, 0, 0, 0))
c_bt.background_normal = join("data", "backgrounds",
"bt_process.png")
c_bt.background_down = join("data", "backgrounds", "count_bt.png")
c_bt.bind(on_release=lambda x: self.dialog_partition_files(bt.text))
# Create edition button
ed_bt = Button(size_hint=(None, None), width=30,
height=30, id="%s?" % part_name, border=(0, 0, 0, 0))
ed_bt.background_normal = join("data", "backgrounds",
"edit_bt_down.png")
ed_bt.background_down = join("data", "backgrounds", "edit_bt.png")
ed_bt.bind(on_release=self.dialog_partitions)
return bt, c_bt, ed_bt
[docs] def set_codon_model(self, codon_partition, wgt=None):
"""
Changes the model spinners when changing the codon partitioning
:param codon_partition: string. The codon partition string that
should correspond to a key in partition_model
:param wgt: Widget object containing the partitions dialog
"""
first_background = join("data", "backgrounds", "model_bt1.png")
second_background = join("data", "backgrounds", "model_bt2.png")
third_background = join("data", "backgrounds", "model_bt3.png")
partition_model = {
"[color=ff5555ff]1[/color] + [color=37abc8ff]2[/color] + "
"[color=71c837ff]3[/color]":
[ModelSpinner(background_normal=first_background, id="1"),
ModelSpinner(background_normal=second_background, id="2"),
ModelSpinner(background_normal=third_background, id="3")],
"[color=ff5555ff](1 + 2)[/color] + [color=37abc8ff]3[/color]":
[ModelSpinner(background_normal=first_background, id="12"),
ModelSpinner(background_normal=second_background, id="3")],
"[color=ff5555ff]1[/color] + [color=37abc8ff](2 + 3)[/color]":
[ModelSpinner(background_normal=first_background, id="1"),
ModelSpinner(background_normal=second_background, id="23")],
"[color=ff5555ff](1 + 3)[/color] + [color=37abc8ff]2[/color]":
[ModelSpinner(background_normal=first_background, id="13"),
ModelSpinner(background_normal=second_background, id="2")],
"No partitions": [ModelSpinner(id="0")]}
if wgt:
partitions_wgt = wgt
else:
partitions_wgt = [x for x in self.root_window.children if
isinstance(x, PartitionsDialog)][0]
partitions_wgt.ids.model_bx.clear_widgets()
for model in partition_model[codon_partition]:
partitions_wgt.ids.model_bx.add_widget(model)
[docs] def save_model(self, part_name, partition_wgt, apply_all=False):
"""
Saves the model currently set in the partitions dialog.
:param part_name: string, name of partition
:param partition_wgt: Widget of the Partitions dialog
:param apply_all: boolean, whether the current model will be
applied to all partitions or not
"""
model_spiners = [x for x in partition_wgt.ids.model_bx.children]
if len(model_spiners) == 1:
self.alignment_list.partitions.set_model(
part_name, [model_spiners[0].text], apply_all=apply_all)
else:
models = []
links = []
for wgt in model_spiners:
models.extend([wgt.text] * len(wgt.id))
links.append(wgt.id)
links, models = [list(x) for x in zip(
*sorted(zip(links, models), key=lambda pair: pair[0]))]
self.alignment_list.partitions.set_model(
part_name, models, links, apply_all=apply_all)
[docs] def remove_partition_box(self):
"""
Removes a currently active partition box when clicking the X button
"""
# Gathers active partition box and remove button
partition_box = [x for x in self.root_window.children if
isinstance(x, PartitionsDialog) or
isinstance(x, RemoveFloat)]
# Removes widgets:
for wgt in partition_box:
self.root_window.remove_widget(wgt)
[docs] def dialog_partition_files(self, partition_name):
"""
Shows a popup listing the files associated with a given partition
:param partition_name: string, name of partition
:return:
"""
content = BtList(cancel=self.dismiss_popup)
plist = self.alignment_list.partitions.partitions_alignments[
partition_name]
for f in plist:
bt = TFButton(text=basename(f), height=40)
content.ids.rev_inlist.add_widget(bt)
self.show_popup(title="Alignment files in %s" % partition_name,
content=content, size_hint=(.6, .8))
[docs] def dialog_select_taxa_group(self):
"""
Shows a subpopup listing the taxa groups that have already been
created. Each taxa group will be a Button widget
"""
content = BtList(cancel=self.dismiss_subpopup)
if self.taxa_groups:
for nm in self.taxa_groups:
bt = TFButton(text=nm, size_hint_y=None, height=30)
bt.bind(on_release=self.select_taxa_group)
content.ids.rev_inlist.add_widget(bt)
else:
bt = TFButton(text="No groups defined", disabled=True)
content.ids.rev_inlist.add_widget(bt)
self.show_popup(title="Select taxa group", content=content,
size_hint=(.4, .8), popup_level=2)
[docs] def select_taxa_group(self, bt):
"""
Gives functionality to the buttons in the dialog_select_taxa_group.
Saves the taxa group name and closes subpopup
:param bt: Button object
"""
if self.taxa_filter_settings:
self.taxa_filter_settings[1] = bt.text
else:
self.taxa_filter_settings = ["Contain", bt.text]
self.dismiss_subpopup()
[docs] def dialog_create_group_from_file(self, ds_type):
"""
Creates a filechooser dialog to select a file containing a
taxa/file list that will be used to generate a data set group
:param ds_type: string. Identifies the data set type. Either taxa or
files
"""
content = SaveDialog(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
content.ids.txt_box.clear_widgets()
content.ids.txt_box.height = 0
title = "Choose text file to import"
content.ids.sd_filechooser.text = ds_type
self.show_popup(title=title, content=content)
[docs] def dialog_select_from_file(self):
"""
Creates a filechooser dialog to select a text file containing a list
of files or taxa names to be selected in the side panel
Calls: select_bt_from_file
"""
content = SaveDialog(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
content.ids.txt_box.clear_widgets()
content.ids.txt_box.height = 0
title = "Choose text file to import"
content.ids.sd_filechooser.text = "select_from_file"
self.show_popup(title=title, content=content)
[docs] def dialog_remove_from_file(self):
"""
Creates a filechooser dialog to select a text file containing a list
of files or taxa names to be removed in the side panel.
Then calls: self.remove_bt_from_file
"""
content = SaveDialog(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
content.ids.txt_box.clear_widgets()
content.ids.txt_box.height = 0
title = "Choose text file to import"
content.ids.sd_filechooser.text = "remove_from_file"
self.show_popup(title=title, content=content)
[docs] def dialog_import_partitions(self):
"""
Creates a filechooser dialog to select a partition file and import
its scheme to the current partition. If one or more partitions
are active, ask the user if we wants to import the partition
scheme to the selected partitions or to the whole dataset
:return:
"""
content = SaveDialog(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
content.ids.txt_box.clear_widgets()
content.ids.txt_box.height = 0
title = "Choose partition scheme file"
content.ids.sd_filechooser.text = "import_partitions"
self.show_popup(title=title, content=content)
[docs] def dialog_partitions(self, btx):
"""
Shows a small widget with partition information
:param btx: Button widget that will be used to determine which
partition is being viewed and the position of the partitions dialog
"""
def flatter(s):
"""
Creates a flat iterator of tuples. If s is [[(1,2), (2,3)],
(4,5)] this will yield ((1,2), (2,3), (4,5))
:param s: list.
"""
for y in s:
if isinstance(y, tuple):
yield y
else:
for j in y:
yield j
partition_model = {
"1,2,3":
"[color=ff5555ff]1[/color] + [color=37abc8ff]2[/color]"
" + [color=71c837ff]3[/color]",
"12,3":
"[color=ff5555ff](1 + 2)[/color] + [color=37abc8ff]3"
"[/color]",
"1,23":
"[color=ff5555ff]1[/color] + [color=37abc8ff](2 + 3)"
"[/color]",
"13,2":
"[color=ff5555ff](1 + 3)[/color] + [color=37abc8ff]2"
"[/color]"}
# Get position of partition edit button:
ed_pos = btx.to_window(btx.pos[0], btx.pos[1])
# Set position for partitions dialog
size = (240, 260)
pos = [ed_pos[0] + btx.width,
ed_pos[1] + (btx.height / 2) - (size[1] / 2)]
content = PartitionsDialog(pos=pos, size=size,
size_hint=(None, None))
rm_wgt = RemoveFloat(pos=[pos[0] + size[0] - 20,
pos[1] + size[1] - 20])
# Set partition object and partition name
# Since partition names can be changed and I can only get the
# partition name from he edition button id (which does not
# change), this iteration over all three partition buttons for
# each partition will retrieve the correct partition name
displayed_partitions = (
x for x in self.root.ids.partition_sl.children if not
isinstance(x, LoadMoreBt))
part_name = [bt.text for ebt, _, bt in
zip(*[iter(displayed_partitions)] * 3)
if ebt.id == btx.id][0]
part_obj = self.alignment_list.partitions
content.ids.partition_name.text = part_name
content.original_name = part_name
# Get partition length
part_range = (y[0] for x, y in self.alignment_list.partitions
if x == part_name)
part_len = sum([x[1] - x[0] + 1 for x in flatter(part_range)])
content.ids.partition_lenght.text = "{}bp".format(part_len)
# If there are codon partitions
if part_obj.partitions[part_name][1]:
if not part_obj.models[part_name][2]:
content.ids.codon_spin.text = content.ids.codon_spin.\
values[1]
self.set_codon_model(
content.ids.codon_spin.values[1], content)
else:
m_key = ",".join(part_obj.models[part_name][2])
content.ids.codon_spin.text = partition_model[m_key]
self.set_codon_model(partition_model[m_key], content)
if part_obj.models[part_name][0][0]:
for i in range(len(part_obj.models[part_name][0])):
params = part_obj.models[part_name][0][i]
model = part_obj.get_model_name(params)
content.ids.model_bx.children[i].text = model
else:
for p, m in enumerate(part_obj.models[part_name][1]):
if m:
content.ids.model_bx.children[::-1][p].text = m
elif part_obj.models[part_name][0][0]:
params = part_obj.models[part_name][0][0]
model = part_obj.get_model_name(params)
content.ids.model_bx.children[0].text = model
elif part_obj.models[part_name][1][0]:
model = part_obj.models[part_name][1][0]
content.ids.model_bx.children[0].text = model
# Give functionality to remove button
rm_wgt.bind(on_release=lambda y: self.remove_partition_box())
self.root_window.add_widget(content)
self.root_window.add_widget(rm_wgt)
[docs] def change_taxa_name(self, old_name, new_name):
"""
Changes the taxa name on a double tap
:param old_name: string, original taxon name
:param new_name: string, new taxon name
"""
self.alignment_list.change_taxon_name(old_name, new_name)
# Change active taxa list
self.active_taxa_list = [new_name if x == old_name else x for x
in self.active_taxa_list]
# Change sp_taxa_bts attribute
for taxa, inf, rm in self.sp_taxa_bts:
if taxa.text == old_name:
taxa.text = new_name
taxa.id = new_name
inf.id = "{}?".format(new_name)
rm.id = "{}X".format(new_name)
# Change tx_info attributes
self.original_tx_inf = dict((new_name, y) if x == old_name else
(x, y) for x, y in self.original_tx_inf.items())
# Change mouser_over_bts attribute
for taxa in [x for x in self.mouse_over_bts["Taxa"]
if isinstance(x, ToggleButton)]:
if taxa.text == old_name:
taxa.text = new_name
self.sidepanel_clear_search("taxa")
[docs] def export_names(self, path, file_name):
"""
Export the names of buttons in the corresponding tab in the side
panel It listens to the self.export_mode attribute, which is a
tuple object with the first element being either "file" or "taxa"
and the second element as "all" or "selected".
:param path: string. Path to the output file.
:param file_name. Name of the output file.
"""
# Create file object
export_file = open(join(path, file_name) + ".txt", "w")
if self.export_mode[0] == "files":
# Export all files
if self.export_mode[1] == "all":
fl = self.file_list if self.file_list else self.filename_map
for x in fl:
short_name = basename(x)
export_file.write(short_name + "\n")
# Export selected files
elif self.export_mode[1] == "selected":
fl = self.active_file_list if self.active_file_list else \
self.active_proteome_files
for x in fl:
short_name = basename(x)
export_file.write(short_name + "\n")
elif self.export_mode[0] == "taxa":
# Export all taxa
if self.export_mode[1] == "all":
for x in self.alignment_list.taxa_names:
export_file.write(x + "\n")
# Export selected taxa
elif self.export_mode[1] == "selected":
for x in self.active_taxa_list:
export_file.write(x + "\n")
# Close file handle
export_file.close()
# Issue success message
self.dialog_floatcheck("Successfully exported {} {}.".format(
self.export_mode[1], self.export_mode[0]),
t="info")
[docs] def toggle_multi_selection(self, value):
"""
Adds multiple selection using shift+click for general purpose lists
of toggle buttons in data set creator
"""
def get_selection(bt, bt_list):
if self.is_shift_pressed:
if not self.prev_tb:
return [bt]
# For possible exceptions where prev_tb is not in button
# list, return only the selected button and reset the prev_tb
# attribute
elif self.prev_tb not in bt_list:
self.prev_tb = None
return [bt]
start = bt_list.index(self.prev_tb)
stop = bt_list.index(bt)
if start < stop:
return bt_list[start:stop + 1]
else:
return bt_list[stop:start + 1]
else:
return [bt]
# Get the parent layout object
parent_obj = value.parent
bt_list = parent_obj.children[::-1]
sel = get_selection(value, bt_list)
for bt in sel:
if value.state == "normal":
bt.state = "normal"
elif value.state == "down":
bt.state = "down"
# Update labels for data set creator dialog
# Get dialog
if isinstance(self._popup.content, TaxaGroupDialog):
dlg = self._popup.content
else:
dlg = self._subpopup.content
if parent_obj.text == "all":
lbl = dlg.ids.all_lbl
else:
lbl = dlg.ids.select_lbl
lbl.text = "{} of {} selected".format(len([x for x in bt_list if
x.state == "down"]),
len(bt_list))
self.prev_tb = value
[docs] def sidepanel_invert_selection(self, panel):
"""
Parameters
----------
panel
Returns
-------
"""
# Set gridlayout and taxa/file list of the appropraite panel
if panel == "Files":
grid_wgt = self.root.ids.file_sl
lst = self.file_list if self.file_list else self.proteome_files
active_lst = self.active_file_list if self.file_list \
else self.active_proteome_files
else:
grid_wgt = self.root.ids.taxa_sl
lst = self.alignment_list.taxa_names
active_lst = self.active_taxa_list
# Storage of active itens
active_items = []
# Invert selection and get the active elements
for item in lst:
if item not in active_lst:
active_items.append(item)
# Update panel with new active elements
for wgt in grid_wgt.children:
# Get only toggle buttons
if isinstance(wgt, ToggleButton):
# Get text. File togglebuttons have the basename of the
# active list
txt = self.filename_map[wgt.text] if panel == "Files" \
else wgt.text
if txt in active_items:
wgt.state = "down"
else:
wgt.state = "normal"
# Core changes to taxa
if panel == "Taxa":
self.active_taxa_list = active_items
self.update_sp_label()
elif panel == "Files":
# Update when alignment files are loaded
if self.file_list:
# update active file list
self.active_file_list = active_items
# Update AlignmentList object
self.alignment_list.update_active_alignments(
active_items
)
# Update when proteome files are loaded
if self.proteome_files:
self.active_proteome_files = active_items
self.update_file_label()
[docs] def toggle_selection(self, value):
"""
Adds functionality for the file and taxa toggle buttons in the side
panel. It adds or removes the selected taxa from the active lists
:param value: Button widget.
"""
def get_selection(bt, tab):
"""
To support multiple selection using shift+click,
this function will return the list of buttons to be modified,
even if it is a single one (when shift is not being pressed)
:param bt: ToggleButton object from sidepanel
:param tab: string, identifier of current tab. Either File,
Taxa or Partitions
"""
if self.is_shift_pressed:
# If there is no previous pressed button in the current
# the behaviour is as if shift is not being pressed
if not self.last_sp_bt[tab]:
return [bt]
start = self.mouse_over_bts[tab].index(self.last_sp_bt[tab])
stop = self.mouse_over_bts[tab].index(bt)
if start < stop:
return self.mouse_over_bts[tab][start + 1:stop + 1]
else:
return self.mouse_over_bts[tab][stop:start]
else:
return [bt]
# Get the parent layout object
parent_obj = value.parent
if self.touch.is_double_tap and parent_obj == self.root.ids.taxa_sl:
self.dialog_text(
"Change taxon name", "change_taxon", value.text)
# determine active file list
act_lst = self.active_file_list if self.file_list else \
self.active_proteome_files
# Changes concerning the files tab
if parent_obj == self.root.ids.file_sl:
sel = get_selection(value, "Files")
for b in sel:
# When button is normal (unselected) remove from active list
if value.state == "normal":
try:
if self.active_file_list:
aln_path = self.filename_map[b.id]
self.alignment_list.update_active_alignment(
aln_path, "shelve")
act_lst.remove(self.filename_map[b.id])
b.state = "normal"
except ValueError:
pass
# When button is down (selected) add to active list
elif value.state == "down":
if self.filename_map[b.id] not in act_lst:
act_lst.append(self.filename_map[b.id])
b.state = "down"
if self.active_file_list:
aln_path = self.filename_map[b.id]
self.alignment_list.update_active_alignment(
aln_path, "active")
# Update label
self.update_file_label()
# Update last pressed button
self.last_sp_bt["Files"] = value
# Changes concerning the taxa tab
elif parent_obj == self.root.ids.taxa_sl:
sel = get_selection(value, "Taxa")
for b in sel:
# When button is normal (unselected) remove from active list
if value.state == "normal":
try:
self.active_taxa_list.remove(b.text)
b.state = "normal"
except ValueError:
pass
# When button is down (selected) add to active
elif value.state == "down":
if b.text not in self.active_taxa_list:
self.active_taxa_list.append(b.text)
b.state = "down"
# Update label
self.update_sp_label()
# Update last pressed button
self.last_sp_bt["Taxa"] = value
elif parent_obj == self.root.ids.partition_sl:
sel = get_selection(value, "Partitions")
for b in sel:
if value.state == "normal":
try:
self.active_partitions.remove(b.text)
b.state = "normal"
except ValueError:
pass
else:
if b.text not in self.active_partitions:
self.active_partitions.append(b.text)
b.state = "down"
# Update last pressed button
self.last_sp_bt["Partitions"] = value
self.update_partition_label()
[docs] def remove_all(self):
"""
Functionality for the remove all button for taxa and file buttons
in the side panel. This method will remove all files and taxa
from the program
"""
# App changes
# Clear widgets in side panel
for panel in [self.root.ids.file_sl, self.root.ids.taxa_sl,
self.root.ids.partition_sl]:
panel.clear_widgets()
self.root.ids.sp_lab.text = ""
self.root.ids.file_lab.text = ""
self.root.ids.partition_lab.text = ""
self.count_files = 0
self.count_partitions = 0
self.clear_process_input()
self.clear_orto_input()
# Add disabled no changes button
if "file_temp" not in [x.id for x in
self.root.ids.file_sl.children]:
no_bt = Button(id="file_temp", text="No files loaded",
size_hint_y=None, height=40, disabled=True)
self.root.ids["file_temp"] = no_bt
self.root.ids.file_sl.add_widget(no_bt)
# Add disabled no changes button
if "species_temp" not in [x.id for x in
self.root.ids.taxa_sl.children]:
no_bt = Button(id="species_temp", text="No files loaded",
size_hint_y=None, height=40, disabled=True)
self.root.ids["species_temp"] = no_bt
self.root.ids.taxa_sl.add_widget(no_bt)
[docs] def remove_all_groups(self):
"""
Removes all loaded orthology groups
"""
# Clear gridlayout contents
for gl in self.screen.ids.orto_group_glbx.children:
gl.clear_widgets()
# Clear orthology cards
self.screen.ids.card_gl.clear_widgets()
# Resets ortho_groups object
# Formal clearing of ortho_groups and temporary files
self.ortho_groups.clear_groups()
# Resets the ortho_groups variable so that new loadings will start anew
self.ortho_groups = None
self.ortho_group_files = []
#self.proteome_files = []
[docs] def remove_groups(self, value):
"""
Removes orthology group buttons
:param value: Instance of remove button
"""
# Remove group from MultiGroup object
self.ortho_groups.remove_group(value.id)
# Remove from ortho group file container
self.ortho_group_files.remove(value.id)
# If all groups have been removed, reset ortho_groups attribute
if not self.ortho_group_files:
self.ortho_groups = None
# Get box container of all gridlayouts
gl_bx = value.parent.parent
for gl in gl_bx.children:
# Remove appropriate item, according to id, from its gridlayout
gl.remove_widget([x for x in gl.children if
x.id == value.id][0])
# If no group button is active, dispatch the first
if not [x for x in self.screen.ids.group_gl.children
if x.state == "down"] and self.screen.ids.group_gl.children:
self.screen.ids.group_gl.children[-1].dispatch("on_release")
self.screen.ids.group_gl.children[-1].state = "down"
if not self.screen.ids.group_gl.children:
self.screen.ids.card_gl.clear_widgets()
[docs] def remove_bt(self, value, parent_wgt=None):
"""
Functionality for the "X" remove buttons in the side panel. It
removes button pairs with similar id's and can be used in both files
and taxa tabs
:param value: Button widget to be removed
:param parent_wgt: Button widget contained. If provided, it
overrides value.parent
"""
# APP CHANGES
# Get the parent layout object from where the widget will be removed
if parent_wgt:
parent_obj = parent_wgt
else:
parent_obj = value.parent
# If the button is the last file or taxa element, issue a remove_all
if parent_obj == self.root.ids.file_sl:
if len(parent_obj.children) == 3 and len(self.file_list) == 1:
return self.remove_all()
elif parent_obj == self.root.ids.taxa_sl:
if len(parent_obj.children) == 3 and \
len(self.alignment_list.taxa_names) == 1:
return self.remove_all()
# Get button widgets to be removed
bt_idx = value.id[:-1]
inf_idx = value.id[:-1] + "?"
c_idx = value.id[:-1] + "C"
# Remove button widgets (name button, info button and remove button)
try:
bt = [x for x in parent_obj.children if bt_idx == x.id][0]
parent_obj.remove_widget(bt)
# Removes reference to this file/taxa in button and mouse over
# vars
if parent_obj == self.root.ids.file_sl:
self.sp_file_bts = [x for x in self.sp_file_bts
if x[0].text != bt.text]
self.mouse_over_bts["Files"].remove(bt)
elif parent_obj == self.root.ids.taxa_sl:
self.sp_taxa_bts = [x for x in self.sp_taxa_bts
if x[0].text != bt.text]
self.mouse_over_bts["Taxa"].remove(bt)
except (IndexError, ValueError):
pass
try:
inf_bt = [x for x in parent_obj.children if inf_idx == x.id][0]
parent_obj.remove_widget(inf_bt)
except IndexError:
pass
try:
cbt = [x for x in parent_obj.children if c_idx == x.id][0]
parent_obj.remove_widget(cbt)
except IndexError:
pass
# Remove widgets
parent_obj.remove_widget(value)
# CORE CHANGES
# Get the parent tab
if parent_obj == self.root.ids.file_sl:
# Set the update_taxa attribute to True so that it updates
# the next time the taxa information is queried
self.trigger_taxa_update = True
# Update file list
file_path = self.filename_map[bt_idx]
del self.filename_map[bt_idx]
# If alignment remove from file list
if self.file_list:
self.file_list.remove(file_path)
# Update alignment object list
self.alignment_list.remove_file([file_path])
# If proteome remove, from proteome file
if self.proteome_files:
self.proteome_files.remove(file_path)
# In proteome files, if the total number of proteomes is lower
# than the set min taxa representation filter, update the
# filter
if len(self.proteome_files) < self.orto_min_sp:
self.orto_min_sp = len(self.proteome_files)
# Update active file list. If the file has been removed from the
# active list, this will handle the exception
try:
self.active_file_list.remove(file_path)
except ValueError:
pass
try:
self.active_proteome_files.remove(file_path)
except ValueError:
pass
# Update active taxa list. This must be executed before calling
# self.get_taxa_information since this method relies on an
# updated active taxa list
self.update_taxa()
# Updates the partition list
self.update_partitions()
# Updates labels
self.update_file_label()
self.update_sp_label()
self.update_partition_label()
if parent_obj == self.root.ids.taxa_sl:
self.alignment_list.remove_taxa([bt_idx])
try:
self.active_taxa_list.remove(bt_idx)
except ValueError:
pass
# Updates label
self.update_sp_label()
if not self.file_list and not self.proteome_files:
self.clear_process_input()
[docs] def remove_bt_from_selection(self, idx, txt_file=None):
"""
Adds functionality to the dropdown button options for removing file
or taxa buttons contained in a text file
:param idx: string, either 'Files' or 'Taxa'.
:param txt_file: string, path to txt file containing the files/taxa
names to be selected. If this is not provided, then the selected
files/taxa will be removed instead
"""
self.dismiss_popup()
selection = []
selection_idx = []
# Get sidepanel widget where items will be removed
wgt = self.root.ids.taxa_sl if idx == "Taxa" else self.root.ids.file_sl
# Grab selection from text file
if txt_file:
with open(txt_file) as fh:
for line in fh:
if line.strip() != "":
selection.append(line.strip())
selection_idx.append(line.strip() + "X")
# Grab selection from selected items in the sidepanel
else:
selection = self.active_taxa_list if idx == "Taxa" else \
[basename(x) for x in self.active_file_list]
selection_idx = [x + "X" for x in selection]
if idx == "Taxa":
missing_elements = [x for x in selection if x not in
self.alignment_list.taxa_names]
# All taxa were selected, so remove all
if not [x for x in self.alignment_list.taxa_names
if x not in selection]:
return self.remove_all()
else:
# If a file does not exist in the app, add it here
missing_elements = [x for x in selection
if x not in self.filename_map]
# All files were selected, so remove all
if not [x for x in self.filename_map if x not in selection]:
return self.remove_all()
# Removes buttons from side panel. In the case of Files, some may
# not be loaded as a button in the side panel (due to the limit
# of 20 files by default). These files have to be removed from
# other structures in the code below
it = iter([(x, x.id) for x in wgt.children])
for bt, idx in it:
if idx in selection_idx:
self.remove_bt(bt, parent_wgt=wgt)
selection_idx.remove(idx)
for idx in selection_idx:
if idx[:-1] in self.filename_map:
# Set the update_taxa attribute to True so that it updates
# the next time the taxa information is queried
self.trigger_taxa_update = True
file_path = self.filename_map[idx[:-1]]
del self.filename_map[idx[:-1]]
self.file_list.remove(file_path)
try:
self.active_file_list.remove(file_path)
except ValueError:
pass
self.alignment_list.remove_file([file_path])
self.update_taxa()
self.update_partitions()
self.update_file_label()
self.update_sp_label()
self.update_partition_label()
# If some of the files did not exist in the app, issue a warning
if missing_elements:
self.dialog_floatcheck("Warning: {} items could not be removed "
"because they were not loaded into the "
"app".format(len(missing_elements)), t="error")
[docs] def select_bt_from_file(self, idx, txt_file):
"""
Adds functionality to the dropdown button options for selecting
file or taxa buttons contained in a text file
:param idx: string, either 'Files' or 'Taxa'.
:param txt_file: string, path to txt file containing the files/taxa
names to be selected
"""
self.dismiss_popup()
selection = []
with open(txt_file) as fh:
for line in fh:
if line.strip() != "":
selection.append(line.strip())
if idx == "Taxa":
for bt in [x for x in self.root.ids.taxa_sl.children if
isinstance(x, ToggleButton)]:
if bt.text in selection:
bt.state = "down"
else:
bt.state = "normal"
self.active_taxa_list = [x for x in selection if x in
self.alignment_list.taxa_names]
self.update_sp_label()
elif idx == "Files":
for bt in [x for x in self.root.ids.file_sl.children if
isinstance(x, ToggleButton)]:
if bt.text in selection:
bt.state = "down"
else:
bt.state = "normal"
if self.file_list:
self.active_file_list = [
self.filename_map[x] for x in
selection if x in self.filename_map]
self.alignment_list.update_active_alignments(
[self.filename_map[x] for x in selection if
x in self.filename_map])
elif self.proteome_files:
self.active_proteome_files = [
self.filename_map[x] for x in
selection if x in self.filename_map]
self.update_file_label()
[docs] def select_bt(self, value):
"""
Functionality to the Select All/Deselect All buttons of the side
panel. The method was made in such a way that it could be of general
use for buttons in the files and taxa tabs
:param value: Button widget.
"""
sv_parent = [x for x in value.parent.parent.parent.children if
isinstance(x, ScrollView)][0]
# This will iterate over the first child of the parent scrollview.
# Since scroll view only supports one child, this should be fine
for i in sv_parent.children[0].children:
# Skips the X buttons
if isinstance(i, ToggleButton):
if value.text == "Select All":
# App related action
i.state = "down"
elif value.text == "Deselect All":
# App related action
i.state = "normal"
# Core changes to files
if (sv_parent == self.root.ids.sv_file and
value.text == "Select All"):
if self.file_list:
self.active_file_list = self.file_list[:]
self.alignment_list.update_active_alignments(
[x for x in self.file_list])
elif self.proteome_files:
self.active_proteome_files = self.proteome_files[:]
# Core changes to taxa
if sv_parent == self.root.ids.sv_sp and value.text == "Select All":
self.active_taxa_list = deepcopy(
self.alignment_list.taxa_names)
# Core changes to files
if (sv_parent == self.root.ids.sv_file and
value.text == "Deselect All"):
if self.file_list:
self.active_file_list = []
self.alignment_list.update_active_alignments([])
elif self.proteome_files:
self.active_proteome_files = []
# Core changes to taxa
if (sv_parent == self.root.ids.sv_sp and
value.text == "Deselect All"):
self.active_taxa_list = []
# Core changes to partitions
if (sv_parent == self.root.ids.sv_partition and
value.text == "Select All"):
self.active_partitions = list(
self.alignment_list.partitions.partitions)
else:
self.active_partitions = []
if (sv_parent == self.root.ids.sv_sp or sv_parent ==
self.root.ids.sv_file):
# Updates labels
self.update_sp_label()
self.update_file_label()
self.update_partition_label()
[docs] def dialog_dataset_creator(self, ds_type, popup_level=1):
"""
Creates a dialog to choose between creating a data set from a file
or manually
:param ds_type: string. Identifier of the data type. Can be either
taxa or files
:param popup_level: integer, The level of the dialog popup. Can be
either 1 (creates _popup instance) or 2 (creates _subpopup instance)
"""
content = DataSetTriageDialog(cancel=self.dismiss_popup)
content.ds_type = ds_type
content.popup_level = popup_level
self.show_popup(title="Choose data set group creation method",
content=content, size=(360, 200))
[docs] def dialog_taxagroup(self, ds_type, popup_level=1):
"""
Creates the layout for the taxa group creation popup.
:param ds_type: string. Data set type. It may be either "taxa" or
"files"
:param popup_level: integer, The level of the dialog popup. Can be
either 1 (creates _popup instance) or 2 (creates _subpopup instance)
"""
# Initializing instance for taxa group dialog
if popup_level == 1:
content = TaxaGroupDialog(cancel=self.dismiss_popup)
else:
content = TaxaGroupDialog(cancel=self.dismiss_subpopup)
if ds_type == "taxa":
bt_list = sorted(self.alignment_list.taxa_names, reverse=True)
title = "Create taxa groups"
group_list = sorted(self.taxa_groups.keys())
else:
bt_list = sorted([basename(x) for x in self.file_list],
reverse=True)
title = "Create file groups"
group_list = sorted(self.file_groups.keys())
# Reset previous button for multi selection support
self.prev_tb = None
# Populate the gridlayout for all entries
for i in bt_list:
# Create togglebutton for each entry
bt = ToggleButton(text=i, size_hint_y=None, height=30)
bt.bind(on_release=self.toggle_multi_selection)
self.add_dataset_bt(bt, content.ids.all_grid, ds_type)
# Populate created groups, if any
if group_list:
content.ids.group_list.clear_widgets()
for i in group_list:
self.taxagroups_add_group(i, content.ids.group_list, ds_type)
content.ds_type = ds_type
# Show dialog
self.show_popup(title=title, content=content, size=(900, 600),
popup_level=popup_level)
[docs] def add_dataset_bt(self, bt, wgt, ds_type):
"""
Method for addition of a button to a widget. This method was created
for the automatic upated of the widgets height when moving buttons
in the taxa group creation dialog
:param bt: The button widget
:param wgt: The sink widget
:param ds_type: string. Data set type. It may be either "taxa" or
"files"
"""
if ds_type == "taxa":
bt_list = sorted(self.alignment_list.taxa_names, reverse=True)
else:
bt_list = sorted([basename(x) for x in self.file_list],
reverse=True)
wgt.add_widget(bt, bt_list.index(bt.text))
wgt.height += 30
@staticmethod
[docs] def remove_taxa_bt(bt, wgt):
"""
Method for addition of a button to a widget. This method was created
for the automatic upated of the widgets height when moving buttons
in the taxa group creation dialog
:param bt: The button widget
:param wgt: The source widget
"""
wgt.remove_widget(bt)
wgt.height -= 30
[docs] def taxagroup_move_taxa(self, source_wgt, sink_wgt, all_taxa, ds_type):
"""
Method that adds functionality to the addition/removal buttons
(<<, <, >>, >) in the taxa group dialog.
:param source_wgt: widget, the gridlayout from where the buttons
will be moved
:param sink_wgt: widget, the gridlayout to where buttons will be
moved
:param all_taxa: Boolean, if True its as if alsa taxa were selected
to be moved
:param ds_type: string. Data set type. It may be either "taxa" or
"files"
"""
if ds_type == "taxa":
bt_list = sorted(self.alignment_list.taxa_names, reverse=True)
else:
bt_list = sorted([basename(x) for x in self.file_list],
reverse=True)
# In case all taxa are to be moved
if all_taxa:
# Ensures that only toggle buttons are moved
for tx in bt_list:
try:
bt = [x for x in source_wgt.children if tx == x.text][0]
self.remove_taxa_bt(bt, source_wgt)
bt.state = "normal"
self.add_dataset_bt(bt, sink_wgt, ds_type)
except IndexError:
pass
else:
# This workaround is used to add some buttons from the source
# to the sink widgets while maintaining their original
# order. The z-index of widgets is not working quite as I
# expected, so for now this will produce the desired behaviour
sink_bts = []
# Remove buttons from the sink widget and store their names into
# a storage list
for bt in sink_wgt.children[::-1]:
self.remove_taxa_bt(bt, sink_wgt)
sink_bts.append(bt.text)
# Gather the buttons from the source widget to be transferred
# while removing them from the sink
for bt in source_wgt.children[::-1]:
if bt.state == "down":
self.remove_taxa_bt(bt, source_wgt)
sink_bts.append(bt.text)
# Add buttons to the sink widget in the desired order
for tx in sorted(sink_bts, reverse=True):
bt = ToggleButton(text=tx, size_hint_y=None, height=30)
bt.bind(on_release=self.toggle_multi_selection)
self.add_dataset_bt(bt, sink_wgt, ds_type)
[docs] def taxagroups_show_taxa(self, name_wgt):
"""
Creates a popup listing the taxa included in a taxa group given by
name
:param name_wgt: widget, widget containing the name of the group as
text
"""
# Create root boxlayout
content = BoxLayout(orientation="vertical", padding=10, spacing=10)
# Create scroll view in which the gridlayout will be inserted
sv = ScrollView(bar_width=10, scroll_type=["bars"])
# Create close button for the popup
close_bt = Button(text="Close", size_hint_y=None, height=30,
bold=True,
background_normal="data/backgrounds/bt_process.png",
bakcground_down="data/backgrounds/bt_process_off.png")
# Add functionality to the close button
close_bt.bind(on_release=self.dismiss_popup)
# Create gridlayout that will store the buttons with taxa names
gl = GridLayout(cols=1, size_hint_y=None, height=0, spacing=5)
# Get ds_type
parent_wgt = name_wgt.parent
# If its taxa data set
if parent_wgt == self.root.ids.taxa_group_grid:
bt_list = self.taxa_groups[name_wgt.text]
else:
bt_list = self.file_groups[name_wgt.text]
# Create buttons
for tx in bt_list:
bt = Button(text=tx, size_hint_y=None, height=30)
gl.add_widget(bt)
gl.height += 35
# Create widget tree
sv.add_widget(gl)
content.add_widget(sv)
content.add_widget(close_bt)
# Show dialog
self.show_popup(title="Taxa group: %s" % name_wgt.text,
content=content, size_hint=(.3, .7))
[docs] def remove_taxa_group(self, rm_wgt):
"""
Removes the data set group button from the app list and
corresponding data set group attribute
:param rm_wgt: widget, widget of the removal button
"""
# Remove from app
parent_wgt = rm_wgt.parent
bt_idx = rm_wgt.id[:-1]
try:
bt = [x for x in parent_wgt.children if bt_idx == x.id][0]
parent_wgt.remove_widget(bt)
parent_wgt.remove_widget(rm_wgt)
except IndexError:
bt = [x for x in parent_wgt.children if bt_idx == x.text][0]
# Remove from the dataset dialog
parent_wgt.remove_widget(bt)
parent_wgt.remove_widget(rm_wgt)
# Remove from the sidepanel
if parent_wgt.ds == "taxa":
for i in [x for x in self.root.ids.taxa_group_grid.children
if x.id == rm_wgt.id or x.id == bt_idx]:
self.root.ids.taxa_group_grid.remove_widget(i)
else:
for i in [x for x in self.root.ids.file_group_grid.children
if x.id == rm_wgt.id or x.id == bt_idx]:
self.root.ids.file_group_grid.remove_widget(i)
# Remove from program attribute
if parent_wgt == self.root.ids.taxa_group_grid or \
parent_wgt.ds == "taxa":
# Remove group from core attribute
del self.taxa_groups[bt_idx]
# Remove button from dropdown menu
# Since the children of a dropdown widget are a gridlayout and
# not the actual buttons contained in the dropdown menu,
# this will search for the children of the gridlayout
for i in [x for x in
self.process_grid_wgt.ids.taxa_dropdown.
children[0].children if x.text == bt_idx]:
self.process_grid_wgt.ids.taxa_dropdown.remove_widget(i)
# Remove button from sidepanel
for i in [x for x in self.root.ids.taxa_group_grid.children
if x.text == bt_idx]:
self.root.ids.taxa_group_grid.remove_widget(i)
if parent_wgt == self.root.ids.file_group_grid or \
parent_wgt.ds == "files":
# Remove group from core attribute
del self.file_groups[bt_idx]
# Remove button from dropdown menu
for i in [x for x in
self.process_grid_wgt.ids.file_dropdown.
children[0].children if x.text == bt_idx]:
self.process_grid_wgt.ids.file_dropdown.remove_widget(i)
# Remove button from sidepanel
for i in [x for x in self.root.ids.file_group_grid.children
if x.text == bt_idx]:
self.root.ids.file_group_grid.remove_widget(i)
[docs] def taxagroups_add_group(self, name, wgt, ds_type):
"""
Adds a dataset button, and corresponding removal button, to the
group list gridlayut of the Dataset dialog.
:param name: string, name of the group
:param wgt: GridLayout widget where the buttons will be added
:param ds_type: string, dataset type, whether 'taxa' or 'files'
"""
bt = ShortenToggleButton(text=name, size_hint_y=None, height=30,
state="normal", disabled_color=(1, 1, 1, 1))
bt.bind(on_release=self.toggle_groups)
bt.bind(on_release=lambda x: self.taxagroups_display_group(name,
ds_type))
rm_bt = Button(size_hint=(None, None), width=30,
height=30, id="{}X".format(name), border=(0, 0, 0, 0),
background_normal=join("data", "backgrounds", "remove_bt.png"),
background_down=join("data", "backgrounds",
"remove_bt_down.png"))
rm_bt.bind(on_release=partial(
self.check_action,
"Are you sure you want to remove this group?",
self.remove_taxa_group,
popup_level=2))
wgt.add_widget(bt)
wgt.add_widget(rm_bt)
[docs] def taxagroups_display_group(self, name, ds_type):
"""
Gives functionality to the group buttons in the dataset dialog. It
displays the group constitution in the dialog
:param name:
:param ds_type:
"""
# Get dataset dialog
root_wgts = self.root_window.children
if [x for x in root_wgts if isinstance(x, Popup)]:
dataset_wgt = [x for x in root_wgts if
isinstance(x, Popup)][0].content
elif [x for x in root_wgts if isinstance(x, CustomPopup)]:
dataset_wgt = [x for x in root_wgts if
isinstance(x, CustomPopup)][0].content
else:
dataset_wgt = None
# Reset dataset creator source and sink gridlayouts
self.taxagroup_move_taxa(dataset_wgt.ids.select_grid,
dataset_wgt.ids.all_grid,
True,
dataset_wgt.ds_type)
# Get group names list
if ds_type == "taxa":
group_lst = self.taxa_groups[name]
else:
group_lst = self.file_groups[name]
# Display group in the selected gridlayout
for i in group_lst:
bt = [x for x in dataset_wgt.ids.all_grid.children
if i == x.text][0]
self.remove_taxa_bt(bt, dataset_wgt.ids.all_grid)
bt.state = "normal"
self.add_dataset_bt(bt, dataset_wgt.ids.select_grid, ds_type)
# Change dataset group name in the dialog
dataset_wgt.ids.group_name.text = name
[docs] def save_dataset_group(self, source_wgt, name, ds_type,
group_file=False):
"""
Adds a taxa group declared using the taxa group creator popup to the
list of taxa groups in the side panel
:param source_wgt, gridlayout of the selected items
:param name: string, name of the group
:param ds_type: string. Data set type. It may be either "taxa" or
"files"
:param group_file: boolean, If True get the group items from the
self.dataset_file file.
"""
# Determine if the Dataset dialog is still active
root_wgts = self.root_window.children
if [x for x in root_wgts if isinstance(x, Popup)]:
dataset_wgt = [x for x in root_wgts if
isinstance(x, Popup)][-1].content
elif [x for x in root_wgts if isinstance(x, CustomPopup)]:
dataset_wgt = [x for x in root_wgts if
isinstance(x, CustomPopup)][-1].content
else:
dataset_wgt = None
# This resets the dataset_wgt if its not the correct instance. This
# happens, for example, when setting data set from file
if not isinstance(dataset_wgt, TaxaGroupDialog):
dataset_wgt = None
if ds_type == "taxa":
# Make core changes by populating self.taxa_groups dictionary
self.taxa_groups[name] = []
group_list = self.taxa_groups[name]
# This list will be used to ensure that when groups are provided
# via text file, only compliant entries are saved
check_list = self.alignment_list.taxa_names
# Set the grid layout where the group button is to be added
grid_layout = self.root.ids.taxa_group_grid
# Set the current dataset group as default for taxa filter if
# it has not been defined
self.taxa_filter_settings[1] = name
else:
# Make core changes by populating self.file_groups dictionary
self.file_groups[name] = []
group_list = self.file_groups[name]
# This list will be used to ensure that when groups are provided
# via text file, only compliant entries are saved
check_list = list(self.filename_map.keys())
# Set the grid layout where the group button is to be added
grid_layout = self.root.ids.file_group_grid
if group_file:
with open(self.dataset_file) as fh:
for line in fh:
if line.strip() in check_list:
group_list.append(line.strip())
else:
for bt in source_wgt.children:
group_list.append(bt.text)
# If dataset dialog is still active, add the new group
if dataset_wgt:
# Remove original button when not groups have been previously
# added
if len(dataset_wgt.ids.group_list.children) == 1:
dataset_wgt.ids.group_list.clear_widgets()
# Add group button to layout
if name not in [x.text for x in
dataset_wgt.ids.group_list.children]:
self.taxagroups_add_group(name, dataset_wgt.ids.group_list,
ds_type)
# Reset dataset creator source and sink gridlayouts
self.taxagroup_move_taxa(dataset_wgt.ids.select_grid,
dataset_wgt.ids.all_grid,
True,
dataset_wgt.ds_type)
for bt in [x for x in dataset_wgt.ids.group_list.children
if isinstance(x, TGToggleButton)]:
bt.state = "normal"
bt.disabled = False
# App changes by adding two buttons for the taxa group
# Taxa button itself
if name not in [x .text for x in grid_layout.children]:
bt = ShortenButton(text=name, size_hint_y=None,
height=30, id=name)
bt.bind(on_release=self.taxagroups_show_taxa)
# Removal button
x_bt = Button(size_hint=(None, None), width=30,
border=(0, 0, 0, 0), height=30, id="%sX" % name,
background_normal=join("data", "backgrounds",
"remove_bt.png"))
x_bt.bind(on_release=partial(self.check_action,
"Are you sure you want to remove this"
" group?",
self.remove_taxa_group))
# Add buttons to gridlayout
for i in [bt, x_bt]:
grid_layout.add_widget(i)
[docs] def projects_init(self):
"""
Initializes the projects attribute
"""
if os.path.exists(self.projects_file):
with open(self.projects_file, "rb") as projects_fh:
# Attempts to read the projects file. If the file is
# somewhat corrupted, remove it and issue a warning
try:
projects_dic = pickle.load(projects_fh)
except EOFError:
os.remove(self.projects_file)
self.projects_init()
# Get ordered dict
projects_dic = OrderedDict(sorted(projects_dic.items(),
key=lambda x: (x[1][1],
x[0])))
# Populate sidepanel
for name, p in projects_dic.items():
project_grid = self.root.ids.project_grid
self.add_project_bt(name, len(p[0]), p[1], project_grid)
# Populates main screen if active
if "home_projects_grid" in self.screen.ids:
project_grid = self.screen.ids.home_projects_grid
self.add_project_bt(name, len(p[0]), p[1],
project_grid)
# This handles cases where the projects file has
# not been populated yet
else:
projects_dic = OrderedDict()
with open(self.projects_file, "wb") as projects_fh:
pickle.dump(projects_dic, projects_fh)
[docs] def save_project(self, name):
"""
Saves the current alignment_list or proteome_list for quick access
to the data sets. It automatically detects whether the current
data set is from the orthology or process modules.
:param name: string, name of the project
"""
# Get projects var
with open(self.projects_file, "rb") as projects_fh:
projects_dic = pickle.load(projects_fh)
# Check for project name duplicates
if name in projects_dic:
return self.dialog_floatcheck(
"Project with the same name is already present",
t="warning")
with open(self.projects_file, "wb") as projects_fh:
if self.file_list:
projects_dic[name] = [self.file_list, "process"]
pickle.dump(projects_dic, projects_fh)
ds_type = "process"
else:
projects_dic[name] = [self.proteome_files, "orthology"]
pickle.dump(projects_dic, projects_fh)
ds_type = "orthology"
project_grid = self.root.ids.project_grid
self.add_project_bt(name, len(self.file_list), ds_type,
project_grid)
if "home_projects_grid" in self.screen.ids:
project_grid = self.screen.ids.home_projects_grid
self.add_project_bt(name, len(self.file_list), ds_type,
project_grid)
[docs] def add_project_bt(self, name, file_num, ds_type, grid_wgt):
"""
Wrapper that adds a project button to the sidepanel
:param name: string, name of the project
:param file_num: int, number of files associated with project
:param ds_type: string, type of dataset. Can be either 'orthology'
or 'process'
:param grid_wgt: GridLayut widget where the project is to be added
"""
if len(grid_wgt.children) == 1:
grid_wgt.clear_widgets()
if ds_type == "orthology":
ds_bt = ProjectOrtoBt(id=name)
else:
ds_bt = ProjectProcBt(id=name)
bt = TFButton(text=name, size_hint_y=None, height=35, bold=True,
id=name)
# Set markup message for project name
msg1 = "[b][color=ccccccff]Project name:[/color][/b] {}".format(
name.encode("utf8"))
# Set markup message for number of files
msg2 = "[b][color=ccccccff]Number of files:[/color][/b] {}".format(
str(file_num))
if ds_bt == "orthology":
pass
else:
bt.bind(on_release=partial(
self.check_action,
[msg1, msg2],
self.open_project,
**{"args": [name],
"check_wgt": CheckProject}))
rm_bt = Button(size_hint=(None, None), size=(35, 35),
id=name, border=(0, 0, 0, 0),
background_normal=join("data", "backgrounds",
"remove_bt35.png"),
background_down=join("data", "backgrounds",
"remove_bt35_down.png"))
rm_bt.bind(on_release=partial(
self.check_action,
"Are you sure you want to remove this project?",
self.remove_project))
for i in [ds_bt, bt, rm_bt]:
grid_wgt.add_widget(i)
[docs] def remove_project(self, wgt):
"""
Gives functionality to the remove project button
:param wgt: remove button
"""
# Remove from projects file
with open(self.projects_file, "rb") as project_fh:
project_dic = pickle.load(project_fh)
del project_dic[wgt.id]
with open(self.projects_file, "wb") as project_fh:
pickle.dump(project_dic, project_fh)
# Remove from sidepanel
for i in [bt for bt in self.root.ids.project_grid.children
if bt.id == wgt.id]:
self.root.ids.project_grid.remove_widget(i)
if len(self.root.ids.project_grid.children) == 0:
self.root.ids.project_grid.add_widget(Button(
text="No Saved Projects", disabled=True, size_hint_y=None,
height=35))
# Remove from home screen, if present
if "home_projects_grid" in self.screen.ids:
for i in [bt for bt in
self.screen.ids.home_projects_grid.children
if bt.id == wgt.id]:
self.screen.ids.home_projects_grid.remove_widget(i)
if len(self.screen.ids.home_projects_grid.children) == 0:
self.screen.ids.home_projects_grid.add_widget(Button(
text="No Saved Projects", disabled=True,
size_hint_y=None, height=35))
[docs] def open_project(self, name):
"""
Closes the current data set and opens the project identified by name
:param name: string, name of the project
"""
with open(self.projects_file, "rb") as projects_fh:
project_dic = pickle.load(projects_fh)
if name in project_dic:
if project_dic[name][1] == "process":
# Closes current data sets
self.remove_all()
# Check if files are present
if [x for x in project_dic[name][0] if not os.path.isfile(
x)]:
self.dialog_floatcheck("Some project "
"files no longer exist", t="warning")
# Opens new dataset
self.load_files_subproc(project_dic[name][0])
if project_dic[name][1] == "orthology":
# Closes current data sets
self.remove_all()
# Opens new data set
self.load_proteomes(project_dic[name][0])
[docs] def more_stats_table_bts(self):
"""
Increases the MAX_TABLE_N attribute by 50 and adds more buttons
to the summary statistics gene table view
"""
# Get results
with open(join(self.temp_dir, "table.pc"), "rb") as fh:
table = pickle.load(fh)
# Remove previous bt
self.stats_table.ids.table_grid.remove_widget(
self.stats_table.ids.table_grid.children[0])
# If the current table has a search operation performed, show more
# lines from that searched table. If not, show more from the original
# table
if any(self.gene_table_selection):
table = self.gene_table_selection
else:
table = self.gene_table
start = self.MAX_TABLE_N
self.MAX_TABLE_N += 25
self.search_add_gene_table_line(table, start)
[docs] def statistics_show_summary(self, force=False, tp="stats"):
"""
Creates a subprocess for calculating summary statistics for the
curently acitve data set. It will show either stats or table
mode, depending on the option provided in the 't' argument.
:param force: Bool, Clear all widgets from Stats scatter and show
summary
:param tp: string, Show overall summary statistics ('stats') or
summary statistics for each gene in table view ('table')
"""
def prepare_display(plt_wgt, scatter):
if self.screen.name == "Statistics":
# Clear main widget
plt_wgt.clear_widgets()
# Remove any potential widgets associated with stats plots
self.screen.ids.footer_box.clear_widgets()
self.dismiss_stats_toggle()
self.dismiss_plot_wgt()
# Remove any potential widgets associated with stats plots
self.screen.ids.footer_box.clear_widgets()
self.dismiss_stats_toggle()
self.dismiss_plot_wgt()
# Disable scale, translation and rotation of scatter
scatter.do_scale = False
scatter.do_translation = False
scatter.do_rotation = False
scatter.scale = 1
scatter.pos = 0, 0
def display_stats(plt_wgt):
# Get results
with open(join(self.temp_dir, "stats.pc"), "rb") as fh:
stats, table = pickle.load(fh)
# Check for exceptions
if "exception" in stats:
return self.dialog_floatcheck(stats["exception"], t="error")
# Populate cards content
for k, v in stats.items():
self.stats_summary.ids[k].text = "%s" % v
# Add stats widget if in Screen
if self.screen.name == "Statistics":
plt_wgt.add_widget(self.stats_summary)
# Set table attribute
self.current_table = table
def display_table(plt_wgt):
# Force statistics side panel close
self.toggle_stats_panel(force_close=True)
# Clear Table grid
self.stats_table = GeneTable()
# Get results
with open(join(self.temp_dir, "table.pc"), "rb") as fh:
table, td = pickle.load(fh)
# Check for exceptions
if "exception" in table:
return self.dialog_floatcheck(table["exception"], t="error")
# Issue warning for large data sets
if len(table) > 50:
self.dialog_floatcheck("Large number of genes to "
"display. Showing first 50 for now. For larger data "
"sets it is recommended to use the 'export table' "
"option", t="warning", dl=10)
self.search_add_gene_table_line(table)
# Add Table widget if in screen
if self.screen.name == "Statistics":
plt_wgt.add_widget(self.stats_table)
# Set table attribute
self.current_table = td
self.gene_table = table
def check_process(p, ldg_wgt, plt_wgt, ns, dt):
try:
ldg.ids.msg.text = "{}/{}".format(ns.counter, ns.files)
except AttributeError:
pass
# KILL SWITCH. Terminating thread by user
if self.terminate_stats:
# Unschedule the current function
Clock.unschedule(check_func)
# Issuing the kill order to the child thread. This signal
# will propagate through the worker's methods and graciously
# terminate
ns.stop = True
# Clear the loading widgets of the Statistics screen
plt_wgt.clear_widgets()
plt_wgt.add_widget(NoDataLabel())
# Remove lock from get_summary_stats method, so that this
# method can be repeated
self.lock_stats = False
# Shutting down manager that provides communication between
# parent and child thread
manager.shutdown()
# Join child process and exit
p.join()
return
# Provides animation for loading widget
if self.current_screen == "Statistics":
if self.screen.ids.stats_panel.width == 0 or \
self.screen.ids.stats_panel.width == 410:
# Sets animation speed
ldg_wgt.ids.img.rotation -= 10
# When worker has finished
if not p.is_alive():
# Set the previous sets as the ones that just finished without
# errors
self.previous_sets["Stats"] = [file_set, taxa_set]
# Display stats depending on the type
prepare_display(plt_wgt, scatter_wgt)
if tp == "stats":
display_stats(plt_wgt)
elif tp == "table":
display_table(plt_wgt)
# Unschedule the current function
Clock.unschedule(check_func)
# Shutting down manager that provides communication between
# parent and child thread
manager.shutdown()
# Joining child thread
p.join()
# Remove lock from get_summary_stats()
self.lock_stats = False
# Exit
return
# Get Scatter widgets
scatter_wgt = self.screen.ids.plot_content
plot_wgt = self.screen.ids.plot_content.children[0]
# If no files have been loaded, ignore
if not self.file_list:
return
# If a process is already running, ignore
if self.lock_stats:
return
# Check if plot has been previously loaded. If yes, ignore.
# If there are no children, proceed as usual
try:
if isinstance(plot_wgt.children[0], Image) and not force:
return
except IndexError:
pass
# Check if any input alignments are active. If not, depends on
# whether there is a summary statistics widget already in
# display. If there is, ignore. If not, use the latest sum stats
# pickle object to quickly display the stats
if not self.active_file_list and self.file_list:
return self.dialog_floatcheck("No active files have been "
"selected", t="error")
# Get currently active data sets
file_set_name = self.screen.ids.active_file_set.text
taxa_set_name = self.screen.ids.active_taxa_set.text
file_set, taxa_set = self.get_active_sets(file_set_name,
taxa_set_name)
# Check if currently active data sets have changed since the last
# calculation of summary statistics
if self.previous_sets["Stats"] == [file_set, taxa_set]:
# Active data sets are the same. If summary stats are already
# displayed, ignore
if isinstance(plot_wgt.children[0], StatsSummary) and \
tp == "stats":
return
elif isinstance(plot_wgt.children[0], GeneTable) and \
tp == "table":
return
# If summary stats are not displayed, then retrieve
# information from the pickle objects and draw the widgets
else:
if tp == "stats":
prepare_display(plot_wgt, scatter_wgt)
display_stats(plot_wgt)
elif tp == "table":
prepare_display(plot_wgt, scatter_wgt)
display_table(plot_wgt)
return
# Prepare screen by removing any potential plot widgets and
# changing some properties of the scatter layout
prepare_display(plot_wgt, scatter_wgt)
# Disable stats subprocess interruption. Being True by default
# and explicitly setting is to False when calculating summary
# stats makes it much more easier to interrupt by other
# actions/widgets
self.terminate_stats = False
self.lock_stats = True
manager = multiprocessing.Manager()
shared_ns = manager.Namespace()
shared_ns.stop = False
shared_ns.counter = 0
p = threading.Thread(target=get_stats_summary,
kwargs={"aln_list": self.alignment_list,
"dest": self.temp_dir,
"active_file_set": file_set,
"active_taxa_set": taxa_set,
"ns": shared_ns})
p.daemon = True
p.start()
# Add loading widget
ldg = StatsLoading()
scatter_wgt.add_widget(ldg)
check_func = partial(check_process, p, ldg,
self.screen.ids.plot_content.children[0],
shared_ns)
Clock.schedule_interval(check_func, .1)
[docs] def search_add_gene_table_line(self, gene_table, start=0):
"""
Wraps the creation of gene table lines into the table_grid widget.
:param gene_table
:return:
"""
cols = ["nsites", "taxa", "var", "inf", "gap", "missing"]
for p, row in enumerate(gene_table.itertuples()):
# Ignore entries before the provided starting point
if p <= start:
pass
if p > self.MAX_TABLE_N:
bt = MoreTableBt()
self.stats_table.ids.table_grid.add_widget(bt)
break
# Create and populate TableLine
x = TableLine()
x.ids.gn_name.text = "{}. {}".format(row.Index + 1, row.genes)
x.ids.gn_name.size_hint_x = 0.5
for idx, val in zip(cols, row[2:]):
x.ids[idx].text = "{}".format(int(val))
# Add TableLine to main grid
self.stats_table.ids.table_grid.add_widget(x)
[docs] def search_statistics_gene_table(self, s):
"""
Searches the summary statistic gene table view, according to the
provided string.
:param s: string. Used to search alignment names.
"""
# Store the master gene table on another attribute, so that it can be
# later retrieved
self.gene_master_table = self.current_table[:]
# Info for new table data
table = [["Gene name", "Number of sites", "Number of taxa",
"Variable sites", "Informative sites", "Gaps",
"Missing data"]]
# clear gene table widget
self.stats_table.ids.table_grid.clear_widgets()
# Search the gene table object for gene names matching the provided
# string
self.gene_table_selection = self.gene_table[
self.gene_table["genes"].str.contains(s)]
self.search_add_gene_table_line(self.gene_table_selection)
# Create new table data
for k in self.gene_table_selection.itertuples():
# Add table line
table.append(list(k[1:]))
self.current_table = table
[docs] def search_statistics_clear(self):
"""
Clears the search in the gene table summary statistics
"""
# Only do this if there is an active selection being displayed
if any(self.gene_table_selection):
# Clear gene_table widget
self.stats_table.ids.table_grid.clear_widgets()
# Re-populate with original genes
self.search_add_gene_table_line(self.gene_table)
# Reset gene_table_selection
self.gene_table_selection = []
# Reset search field hint text
self.stats_table.ids.gn_txt.text = ""
# Set the current_table attribute to the total gene list
self.current_table = self.gene_master_table[:]
[docs] def search_sort_gene_table(self, sortby, ascending=True):
"""
Sorts the gene table by any column header.
:param str sortby: Column header name to perform the sort
:param bool ascending: Whether the sorting will be ascending or
descending
"""
col_map = {"Gene name": "genes",
"N": "nsites",
"Taxa": "taxa",
"V": "var",
"P": "inf",
"Gap": "gap",
"M": "missing"}
# Info for new table data
table = [["Gene name", "Number of sites", "Number of taxa",
"Variable sites", "Informative sites", "Gaps",
"Missing data"]]
# clear gene table widget
self.stats_table.ids.table_grid.clear_widgets()
self.gene_table.sort_values([col_map[sortby]],
ascending=ascending,
inplace=True)
self.search_add_gene_table_line(self.gene_table)
# Create new table data
for k in self.gene_table.itertuples():
# Add table line
table.append(list(k[1:]))
self.current_table = table
[docs] def statistics_populate_groups(self, ds_type):
"""
This method is called when the dataset selection buttons in the
Statistics sidepanel are pressed. They populate the respective
dropdown menu with the currently set groups
:param ds_type: string, data set type. Can be either 'taxa' or
'files'
"""
def set_dd_txt(txt, wgt):
"""
Using this function instead of lamdba to set the callback on the
on_release event of the dropdown buttons ensures that each button
has each unique callback and it's not overwritte (as in the lamdba)
:param txt: string, text provided to select() method
:param wgt: dummy argument, required because of how the bind()
method works
"""
dd_wgt.select(txt)
if ds_type == "taxa":
dd_wgt = self.screen.ids.taxa_dropdown
grid_children = \
self.screen.ids.taxa_dropdown.children[0].children
group_atr = self.taxa_groups
else:
dd_wgt = self.screen.ids.file_dropdown
grid_children = \
self.screen.ids.file_dropdown.children[0].children
group_atr = self.file_groups
# Remove discarded groups
for bt in grid_children:
if bt.text in ["All taxa", "Active taxa", "All files",
"Active files"]:
pass
elif bt.text not in group_atr:
dd_wgt.remove_widget(bt)
# Add new groups
current_groups = [x.text for x in grid_children]
for g in group_atr:
if g not in current_groups:
dd_bt = Button(
text=g, size_hint_y=None, height=40, bold=True,
background_normal=join("data", "backgrounds",
"spinner_opt.png"))
dd_bt.bind(on_release=partial(set_dd_txt, g))
dd_wgt.add_widget(dd_bt)
[docs] def dialog_general_info(self, idx):
"""
Generates the popup with information for several components of the
application
:param idx: string. Identifier of the informative content to be
shown. It must be present in the dictionary keys of the
informative_storage variable in data/resources/info_data.py
"""
content = InfoPopup(cancel=self.dismiss_popup)
# Retrieve title and body text
title_str, body_str = informative_storage[idx]
# Add text body
content.ids.content.text = body_str
self.show_popup(title=title_str, content=content,
size=(500, 400), close_bt=True)
[docs] def operation_queue_init(self):
"""
This will create the skeleton for the tree view of the operations
queued for execution in an appropriate screen of the side panel.
"""
# Create tree view instance
self.operation_tv = TreeView(hide_root=True, size_hint_y=None,
height=269)
# Create main nodes for each module
ortho_node = self.operation_tv.add_node(
TreeViewLabel(text="Orthology Operations", bold=True,
font_size=20, color=(1, 0.3, 0.3, .2)))
proc_node = self.operation_tv.add_node(
TreeViewLabel(text="Process Operations", bold=True,
font_size=20, color=(.3, .3, 1, 1)))
# Main subnodes for Process
main_op_node = self.operation_tv.add_node(TreeViewLabel(
text="Main Operation", bold=True, font_size=15, opacity=.2),
proc_node)
secondary_op_node = self.operation_tv.add_node(TreeViewLabel(
text="Secondary Operations", bold=True, font_size=15,
opacity=.2),
proc_node)
format_node = self.operation_tv.add_node(TreeViewLabel(
text="Output Formats", bold=True, font_size=15, opacity=.2),
proc_node)
main_file = self.operation_tv.add_node(TreeViewLabel(
text="Output File", bold=True, font_size=15, opacity=.2),
proc_node)
# Save main nodes
self.main_nodes = {"ortho": ortho_node, "proc": proc_node,
"proc_main": main_op_node,
"proc_form": format_node,
"proc_sec": secondary_op_node,
"main_file": main_file}
self.root.ids.op_sv.add_widget(self.operation_tv)
[docs] def save_operation_queue(self):
"""
This populate the operations queue tree view with the operations and
options selected by the user
As of now, it listens to information stored in:
- self.main_operations, to gather the main operation
- self.process_switches, to gather secondary operations
- self.output_formats, to gather information on the output
formats
- self.
"""
def clear_nodes(parent):
old_nodes = []
for node in self.operation_tv.iterate_all_nodes(parent):
if node.text != parent.text:
old_nodes.append(node)
for node in old_nodes:
self.operation_tv.remove_node(node)
self.operation_tv.height -= 24
def add_node(text, parent):
self.operation_tv.add_node(
TreeViewLabel(text=text, font_size=16), parent)
self.operation_tv.height += 24
if parent.opacity != 1:
parent.opacity = 1
# PROCESS NODES
# Main operation
# Clear old nodes
clear_nodes(self.main_nodes["proc_main"])
try:
main_op = [nm for nm, bl in self.main_operations.items()
if bl is True][0]
add_node("%s" % main_op, self.main_nodes["proc_main"])
# Open Process node
if self.main_nodes["proc"].is_open is False:
self.operation_tv.toggle_node(self.main_nodes["proc"])
# Open main operation node if closed
if self.main_nodes["proc_main"].is_open is False:
self.operation_tv.toggle_node(self.main_nodes["proc_main"])
except IndexError:
self.main_nodes["proc_main"].opacity = .2
# Output format
# Clear old nodes
clear_nodes(self.main_nodes["proc_form"])
if self.output_formats:
for ft in self.output_formats:
add_node("%s" % ft, self.main_nodes["proc_form"])
# Open output format node is closed
if self.main_nodes["proc_form"].is_open is False:
self.operation_tv.toggle_node(self.main_nodes["proc_form"])
else:
self.main_nodes["proc_form"].opacity = .2
# Secondary operations
# Clear old nodes
clear_nodes(self.main_nodes["proc_sec"])
secondary_op = [nm for nm, bl in self.secondary_operations.items()
if bl is True]
if secondary_op:
for op in secondary_op:
add_node("%s" % op, self.main_nodes["proc_sec"])
if self.main_nodes["proc_sec"].is_open is False:
self.operation_tv.toggle_node(self.main_nodes["proc_sec"])
else:
self.main_nodes["proc_sec"].opacity = .2
# Output file
clear_nodes(self.main_nodes["main_file"])
# for conversion
if self.main_operations["conversion"]:
add_node("[Based on input] (main)",
self.main_nodes["main_file"])
if self.main_nodes["main_file"].is_open is False:
self.operation_tv.toggle_node(self.main_nodes["main_file"])
# Output files from secondary operations
if secondary_op:
for op in secondary_op:
if self.secondary_options["%s_file" % op]:
add_node("*_%s (%s)" % (op, op),
self.main_nodes["main_file"])
# for concatenation
elif self.main_operations["concatenation"]:
if self.output_file == "":
add_node("[empty] (main)", self.main_nodes["main_file"])
else:
add_node("%s (main)" % basename(self.output_file),
self.main_nodes["main_file"])
# Output files from secondary operations
if secondary_op:
for op in secondary_op:
if self.secondary_options["%s_file" % op]:
add_node("%s_%s (%s)" % (basename(self.output_file),
op, op),
self.main_nodes["main_file"])
if self.main_nodes["main_file"].is_open is False:
self.operation_tv.toggle_node(self.main_nodes["main_file"])
else:
self.main_nodes["main_file"].opacity = .2
[docs] def process_clear_options(self):
# CORE CHANGES
# Clear main operations
self.main_operations = dict((op, False) for op in
self.main_operations)
# Clear active data set
self.process_grid_wgt.ids.active_taxa_set.text = "Active taxa"
self.process_grid_wgt.ids.active_file_set.text = "Active files"
# Clear output formats
self.output_formats = ["fasta"]
# Clear filters, consensus, haplotype name and zorro suffix
self.missing_filter_settings = [(True, 25, 50), (True, 0)]
self.taxa_filter_settings = [None, None]
self.codon_filter_settings = [True, True, True]
self.hap_prefix = "Hap"
self.zorro_suffix = ""
self.zorro_dir = ""
self.process_options.ids.consensus_mode.text = \
"IUPAC" if self.sequence_types != "Protein" else "Soft mask"
# Clear output file
self.output_file = ""
# Clear secondary operations
self.secondary_operations = dict((op, False) for op in
self.secondary_operations)
# Clear secondary options
self.secondary_options = dict((op, False) for op in
self.secondary_options)
# APP CHANGES
# Deselect main operation
for bt in ["conv", "conc"]:
self.screen.ids[bt].state = "normal"
self.screen.ids[bt].disabled = False
# Changes in buttons with dynamic text
# Output format
self.process_grid_wgt.ids.conv_formatbt.text = "Fasta"
# Output file
self.process_grid_wgt.ids.conv.text = "Select..."
# Zorro settings
self.process_options.ids.zorro.background_normal = \
"data/backgrounds/bt_process_off.png"
self.process_options.ids.zorro.text = "Off"
# Turn switches off
for switch in self.secondary_operations:
try:
self.process_options.ids[switch].active = False
except KeyError:
pass
for switch in self.secondary_options:
try:
self.process_options.ids[switch].active = False
except KeyError:
pass
[docs] def orthology_clear_options(self):
"""
Resets orthology search options to default values
"""
self.ortho_dir = ""
self.orto_export_dir = ""
self.usearch_db = "goodProteins_db"
self.usearch_output = "AllVsAll.out"
self.usearch_evalue = "0.00001"
self.ortholog_prefix = "MyGroup"
self.group_prefix = "group"
self.mcl_inflation = ["3"]
self.orto_max_gene = 1
self.orto_min_sp = 3
self.screen.ids.usearch_threads.text = "1"
self.ortho_search_options.ids.inflation_bt.text = "['3']"
# ########################### PLOT SCREENS #############################
[docs] def show_stats_toggle(self, args1, args2, active_bt, single_gene=None):
"""
Adds a toggle widget to some Statistics plots that allow the user to
toggle plots between the whole data set and species perspectives
:param args1: dictionary, key must be "plt_idx", and value the
plot index string for the species plot type
:param args2: dictionary, key must be "plt_idx", and value the
plot index string for the average plot type
:param active_bt: string, identifier of the active button. Can be
either "sp" (Species), "avg" (Average) or "gene" (Single gene)
:param single_gene: dictionary, key must be "plt_idx", and value the
plot index string for the single gene plot type
"""
content = StatsToggleWgt()
if not single_gene and active_bt != "gene":
content.remove_widget(content.ids.gene)
content.remove_widget(content.ids.gene_sep)
else:
content.gene_args = single_gene
if active_bt == "gene":
content.ids.gene.background_normal = \
"data/backgrounds/bt_focus.png"
content.ids.gene.text = "Change gene"
else:
content.ids.gene.text = "Single gene"
for wgt_name in ["avg", "sp"]:
if wgt_name == active_bt:
content.ids[wgt_name].state = "down"
content.ids[wgt_name].disabled = True
else:
content.ids[wgt_name].state = "normal"
content.ids[wgt_name].disabled = False
if args2:
content.args2 = args2
else:
content.ids.avg.disabled = True
if args1:
content.args1 = args1
else:
content.ids.sp.disabled = True
self.previous_stats_toggle = content
self.root_window.add_widget(content)
[docs] def show_back_bt(self):
"""
Adds a back button to self.root_window, which will navigate to the
previous screen. This is meant for headless plot screens
"""
bt = BackButton()
self.root_window.add_widget(bt)
[docs] def dismiss_stats_toggle(self):
"""
Removes the stats toggle widget
"""
try:
wgt = [x for x in self.root_window.children if
isinstance(x, StatsToggleWgt)][0]
self.root_window.remove_widget(wgt)
except IndexError:
pass
[docs] def dismiss_plot_wgt(self):
"""
Removes plot widgets from the root window
"""
try:
for wgt in [x for x in self.root_window.children if
isinstance(x, OrtoPlotToolbar) or
isinstance(x, BackButton) or
isinstance(x, StatsPlotToolbar) or
isinstance(x, StatsToggleWgt)]:
self.root_window.remove_widget(wgt)
except IndexError:
pass
# ######################### ORTHOLOGY SCREEN ###########################
[docs] def toggle_orto_soptions(self):
"""
Controls the toggling of the GridLayout with the advanced options
for the Orthology screen, Ortholog search slide
"""
if not self.orto_search_height:
self.orto_search_height = self.screen.ids.gl_orto_search.height
if (self.screen.ids.adv_search_options.text ==
"Show additional options"):
# Add widget to main grid
self.screen.ids.gl_orto_search.add_widget(
self.ortho_search_options)
# Animate widget entrance
Animation(opacity=1, d=.5, t="in_quad").start(
self.ortho_search_options)
# Update button text
self.screen.ids.adv_search_options.text = \
"Hide additional options"
self.screen.ids.gl_orto_search.height = \
self.orto_search_height + sum(x.height + 5 for x in
self.ortho_search_options.ids.mcl_grid.children) + 30
elif self.screen.ids.adv_search_options.text == \
"Hide additional options":
self.screen.ids.gl_orto_search.height = self.orto_search_height
# Remove widget from main grid
self.screen.ids.gl_orto_search.remove_widget(
self.ortho_search_options)
# Update button text
self.screen.ids.adv_search_options.text = \
"Show additional options"
[docs] def dialog_search_report(self, stat_storage, groups):
"""
Creates the dialog that reports the results of the Orthology search
:param stat_storage: dictionary. Each entry corresponds to an
inflation value, which will have a list as a value. The list will
contain:
[total_orts, species_compliant_orts, gene_compliant_orts,
final_orts]
"""
content = OrthoReportDialog(cancel=self.dismiss_popup)
for inf in sorted(self.mcl_inflation):
stats = stat_storage[inf]
# Creating graphical report
report_wgt = OrthoGraphicReport()
# Setting inflation value attribute
report_wgt.inf = inf
# Setting total orthologs
report_wgt.ids.total_ort.text = str(stats[0])
# Setting gene compliant
report_wgt.ids.gf_txt.text = str(stats[2])
report_wgt.ids.gf_box.size_hint_x = float(stats[2]) / \
float(stats[0])
# Setting species compliant
report_wgt.ids.sf_txt.text = str(stats[3])
report_wgt.ids.sf_box.size_hint_x = float(stats[3]) / \
float(stats[0])
# Setting final orthologs
report_wgt.ids.final_txt.text = str(stats[4])
report_wgt.ids.final_box.size_hint_x = float(stats[4]) / \
float(stats[0])
# Adding widget to carousel
content.ids.report_car.add_widget(report_wgt)
content.ids.ok_bt.bind(on_release=lambda x: self.load_groups(
groups, groups.filters))
self.show_popup(title="Orthology search report", content=content,
size=(400, 470))
[docs] def dialog_import_groups(self):
"""
Creates filechooser dialog to select group files to be imported
"""
content = LoadMultipleDialog(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
self.show_popup(title="Choose group file(s) to import",
content=content, size_hint=(.9, .9))
[docs] def dialog_protein_filter(self):
content = ProteinFilterDialog(cancel=self.dismiss_popup)
self.show_popup(title="Protein filter settings", content=content,
size=(350, 200))
[docs] def dialog_inflation(self):
"""
Creates dialog for inflation values selection
"""
content = InflationDialog(cancel=self.dismiss_popup)
# Updated dialog with the selected inflation values
for i in content.ids.inflation_bx.children:
if i.text in self.mcl_inflation:
i.state = "down"
else:
i.state = "normal"
self.show_popup(title="MCL inflation settings", content=content,
size=(300, 220))
[docs] def dialog_orto_setfilter(self, group_name):
"""
A similar dialog to dialog_ortho_filter but for the explore screen.
Contains an additional option of applying the specified filters
to all group files
:param group_name: string. name for the group object
"""
content = OrtoSetFiltersDialog(cancel=self.dismiss_popup)
content.group_name = group_name
sp_list = self.ortho_groups.taxa_list[self.active_group_name]
excluded_tx = self.ortho_groups.excluded_taxa[self.active_group_name]
content.ids.exclude_bt.text = "{} included".format(
len(sp_list))
content.excluded_tx = excluded_tx
self.show_popup(title="Set/change ortholog filters for %s file" %
group_name, content=content, size=(400, 390))
[docs] def dialog_ortho_filter(self):
"""
Creates dialog for orthology cluster filters
"""
content = OrtoFilterDialog(cancel=self.dismiss_popup)
self.show_popup(title="Ortholog filters", content=content,
size=(400, 200))
[docs] def save_ortho_filters(self, gene_filt, sp_filt):
"""
Save orthology clusters filters
:param gene_filt: int. Integer for gene filter threshold
:param sp_filt: int. Integer for species filter threshold
"""
try:
if int(gene_filt) < 0:
return self.dialog_floatcheck(
"Maximum number of gene copies must be higher "
"than 0", t="error")
self.orto_max_gene = int(gene_filt)
except ValueError:
return self.dialog_floatcheck(
"Maximum number of gene copies '{}' must be a real "
"numbers".format(gene_filt), t="error")
try:
if float(sp_filt) < 0:
return self.dialog_floatcheck(
"Minimum number of taxa must be a positive "
"value", t="error")
if 0 < float(sp_filt) < 1:
orto_min_sp = float(sp_filt)
else:
orto_min_sp = int(sp_filt)
except ValueError:
return self.dialog_floatcheck(
"Minimum number of taxa '{}' must be a number".format(
sp_filt), t="error")
# Add check for min species. If this filter is set to a number
# greater that the number of proteome input files (which should
# represent a single species each) this will issue a warning.
if self.proteome_files and \
int(sp_filt) > len(self.active_proteome_files):
return self.dialog_floatcheck("Minimum number of "
"species larger than the provided"
" proteomes", t="warning")
else:
self.orto_min_sp = orto_min_sp
self.dismiss_popup()
return self.orto_max_gene, self.orto_min_sp
[docs] def save_inflation(self, inflation_wgt):
"""
Save inflation values
:param inflation_wgt: Widget. Widget containing the inflation values
"""
for wgt in inflation_wgt.children:
if wgt.state == "down" and wgt.text not in self.mcl_inflation:
self.mcl_inflation.append(wgt.text)
elif wgt.state == "normal" and wgt.text in self.mcl_inflation:
self.mcl_inflation.remove(wgt.text)
self.ortho_search_options.ids.inflation_bt.text = \
str(sorted(self.mcl_inflation))
[docs] def save_protein_filters(self, min_len, max_stop):
"""
Saves protein length and stop percentage filters
:param min_len: int. Minimum sequence length
:param max_stop: int. Maximum percentage of stop codons
"""
# Check arguments for compliance. Must be int
try:
# Check if max_stop value is between 0 and 100
if 0 < float(max_stop) > 100:
return self.dialog_floatcheck(
"Maximum codon stops value must be between 0 "
"and 100", t="error")
self.protein_min_len = abs(int(min_len))
self.protein_max_stop = abs(int(float(max_stop)))
self.dismiss_popup()
except ValueError:
return self.dialog_floatcheck(
"{} and {} must be "
"numbers".format(min_len, max_stop), t="error")
[docs] def dialog_export_groups(self):
"""
Dialog for group exportation.
"""
content = ExportGroupDialog(cancel=self.dismiss_all_popups)
self.show_popup(title="Export group as...", content=content,
size=(600, 360))
[docs] def dialog_export_groups_filechooser(self, idx):
"""
When clicking Export groups in the main dialog for group
exportation, the user is redirected to a filechooser to choose
the output directory and output file name, in the case of group
exportation.
:param idx: string. Determines the export mode: if "protein" or
"nucleotide", it will export sequence files, if "group", it will
export to another group file
"""
content = SaveDialog(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
# Set the path from previously imported groups, if any
content.ids.sd_filechooser.path = unicode(self.orto_export_dir) if \
self.orto_export_dir else unicode(self.home_path)
if idx == "nucleotide" or idx == "protein":
content.ids.txt_box.clear_widgets()
content.ids.txt_box.height = 0
title = "Export sequences to directory ..."
else:
title = "Export group file to directory ..."
content.ids.sd_filechooser.text = idx
self.show_popup(title=title, content=content)
[docs] def orto_export_groups(self, export_idx, output_dir=None,
output_name=None):
"""
This will handle the group exportation of the orthology screen. The
intensive export methods will run in the background while updating
the main process of their process.
:param export_idx: string, with the identifier of exportation. Can
be either 'group', 'protein' or 'dna' the database from which
sequences will be retrieved
:param output_dir: string. Path to output directory
:param output_name: string, for group exportation, provide the name
of the output filtered file
"""
def check_process(p, dt):
# Update dialog text
try:
content.ids.msg.text = shared_ns.act
except AttributeError:
pass
# Update progress bar
try:
content.ids.pb.value = shared_ns.progress
except AttributeError:
pass
# Update maximum progress bar value
try:
content.ids.pb.max = shared_ns.max_pb
except AttributeError:
pass
if self.terminate_group_export:
content.ids.msg.text = "Terminating on you command. " \
"Hang tight!"
# Issuing the kill order to the child thread. This signal
# will propagate through the worker's methods and graciously
# terminate
shared_ns.stop = True
# This small delay seems to fix manager shutdown issues
time.sleep(.1)
# Shutting down manager that provides communication between
# parent and child thread
manager.shutdown()
# Close loading popup dialog
self.dismiss_popup()
# Unschedule the current function
Clock.unschedule(func)
# Join child process and exit
p.join()
return
# Check process status
if not p.is_alive():
try:
if shared_ns.exception:
return self.dialog_floatcheck(
"An unexpected error occurred when exporting "
"orthologs. Check the app logs.", t="error")
except:
pass
Clock.unschedule(func)
self.dismiss_popup()
if not shared_ns.missed:
self.dialog_floatcheck(
"%s orthologs successfully exported" % shared_ns.good,
t="info")
else:
self.dialog_floatcheck(
"%s orthologs exported. However, %s sequences "
"could not be retrieved! Missed sequence headers "
"were stored in missed_sequences.log" %
(shared_ns.good, shared_ns.missed), t="info")
p.join()
# Update orthology export directory, if necessary
if output_dir != self.orto_export_dir:
self.orto_export_dir = output_dir
# Get active_group if not already defined by fetching the active
# group button in the Explore screen
if not self.active_group:
group_id = [x.id for x in self.screen.ids.group_gl.children
if x.state == "down"][0]
self.active_group = self.ortho_groups.get_group(group_id)
# Let the current screen define which filters are going to be used
# for ortholog export.
# If the screen is ORTO_PLOT, then use the fitlers specific to the
# current active group.
# If the screen is ORTHOLOGY, then use the ortho groups general filters
if self.screen.name == "Orthology":
# Update filter values
self.active_group.update_filters(*self.ortho_groups.filters[
self.active_group.name])
self.active_group.exclude_taxa(self.ortho_groups.excluded_taxa[
self.active_group.name])
# Set up manager and shared name space to share information
# between background and main processes
manager = multiprocessing.Manager()
shared_ns = manager.Namespace()
# Initialize stop flag to allow thread to be killed by the user
shared_ns.stop = False
method_store = {
"group":
[self.active_group.export_filtered_group,
[output_name, output_dir]],
"protein":
[self.active_group.retrieve_sequences,
[self.sqldb, self.protein_db, output_dir]],
"nucleotide":
[protein2dna.convert_group,
[self.sqldb, self.cds_db, self.protein_db,
self.active_group, self.usearch_file, output_dir]]}
# Get method and args
m = method_store[export_idx]
# Remove lock from background process
self.terminate_group_export = False
# Create process
p = threading.Thread(target=background_export_groups,
args=(m[0], shared_ns, m[1]))
p.daemon = True
p.start()
# Remove any previous popups
self.dismiss_all_popups()
# Create loading process dialog
content = LoadProgressDialog()
content.ids.pb.max = self.active_group.all_compliant
# Give functionality to cancel button
content.ids.close_bt.bind(
on_release=lambda i: setattr(self, "terminate_group_export",
True))
self.show_popup(title="Exporting...", content=content,
size=(400, 250), auto_dissmiss=False)
# Schedule function that checks the process' pulse
func = partial(check_process, p)
Clock.schedule_interval(func, 1)
[docs] def orto_report_dialog(self):
"""
Generates a filechooser upon clicking on the "Generate full
report" button in Orthology Explore. The filechooser will provide
the directory where the report will be generated
"""
content = SaveDialog(cancel=self.dismiss_popup,
bookmark_init=self.bookmark_init)
# Set the path from previously imported groups, if any
content.ids.sd_filechooser.path = unicode(self.orto_export_dir) if \
self.orto_export_dir else self.home_path
title = "Generate full report in directory..."
# Remove file name text input
content.ids.txt_box.height = 0
content.ids.txt_box.clear_widgets()
# Set identifier for filechooser
content.ids.sd_filechooser.text = "orto_report"
self.show_popup(title=title, content=content)
[docs] def get_active_group_light(self):
if not self.active_group:
group_id = [x.id for x in self.screen.ids.group_gl.children if x.state == "down"][0]
self.active_group = self.ortho_groups.get_group(group_id)
return self.active_group
[docs] def orto_generate_report(self, out_dir, ns=None):
"""
Generates full orthology report on the specified directory.
:param out_dir: string, path to directory where the report will be
generated
"""
# Create directory that will store figures
fig_dir = join(out_dir, "Figures")
if not os.path.exists(fig_dir):
os.makedirs(fig_dir)
if ns:
ns.files = len(MultiGroupsLight.calls)
active_group_light = self.get_active_group_light()
for command in MultiGroupsLight.calls:
if ns:
if ns.stop:
raise KillByUser("")
ns.counter += 1
data = getattr(active_group_light, command)(fig_dir)
fig, lgd, _ = bar_plot(**data)
plot_file = join(fig_dir, command + ".png")
if lgd:
fig.savefig(plot_file, bbox_extra_artists=(lgd,),
bbox_inches="tight", dpi=200)
else:
fig.savefig(plot_file, bbox_inches="tight", dpi=200)
html = HtmlTemplate(out_dir, "Orthology report", orthology_plots)
html.write_file()
self.dialog_floatcheck("Orthology automatic report successfully "
"generated.", t="info")
[docs] def orto_compare_groups(self, groups_objs=None, selected_groups=None):
"""
Switches to the orthology group comparison screen and presents the
initial plot comparing total orthologs across group files
:param groups_objs: MultiGroupLight object. Provide only when
updating filters in the plot screen
:param selected_groups: list. If provided, should contain the name
of the groups that should be plotted.
"""
# Displays correspondence
displays = {"total_ort": "1", "sp_ort": "2", "gn_ort": "3",
"final_ort": "4"}
# Determine MultiGroupLight object
if groups_objs:
groups_objs = groups_objs
else:
groups_objs = self.ortho_groups
# Update slider max values
self.screen.ids.gn_spin.max = \
max(groups_objs.max_extra_copy.values())
self.screen.ids.sp_spin.max = groups_objs.species_number[0]
# Update initial slider values
if not self.screen.ids.header_content.original_filt:
self.screen.ids.gn_spin.value = self.orto_max_gene
self.screen.ids.sp_spin.value = self.orto_min_sp if \
self.orto_min_sp != 3 else groups_objs.species_number[0]
self.screen.ids.header_content.original_filt = \
[self.screen.ids.gn_spin.value, self.screen.ids.sp_spin.value]
self.screen.ids.header_content.excluded_taxa = \
groups_objs.excluded_taxa[self.active_group_name]
# Set group object for screen. This property will be used when
# changing which filters should be displayed in the compare
# plot screen
self.screen.group_obj = groups_objs
# Get active displays
stats = "".join([y for x, y in displays.items()
if self.screen.ids[x].active])
if stats:
# Create first comparison plot of total orthologs
self.current_plot, self.current_lgd, self.current_table = \
groups_objs.bar_orthologs(group_names=selected_groups,
dest=self.temp_dir, stats=stats)
# Load plot
self.load_plot(join(self.temp_dir, "Final_orthologs.png"),
self.screen.ids.plot_content)
else:
self.screen.ids.plot_content.children[0].clear_widgets()
[docs] def dialog_plot_check_filters(self, previous_filt, prev_tx,
current_filt, current_tx):
"""
Dialog that is triggered when exiting a plot screen and the current
plot filters are different from the filters before entering the plot
screen. Will ask the user if the filters should be updated for the
remaining app or not.
"""
# If filters are the same, do nothing and return
if (previous_filt == current_filt) and (prev_tx == current_tx):
self.go_previous_screen()
return
content = PlotChangeFilters()
content.ids.prev_gn.text = str(previous_filt[0])
content.ids.cur_gn.text = str(current_filt[0])
content.prev_filt = previous_filt
content.prev_tx = prev_tx
content.cur_filt = current_filt
content.cur_tx = current_tx
if previous_filt[0] != current_filt[0]:
content.ids.prev_gn.color = self._red
content.ids.cur_gn.color = self._red
content.ids.prev_min_tx.text = str(previous_filt[1])
content.ids.cur_min_tx.text = str(current_filt[1])
if previous_filt[1] != current_filt[1]:
content.ids.prev_min_tx.color = self._red
content.ids.cur_min_tx.color = self._red
content.ids.prev_ex_tx.text = str(len(prev_tx))
content.ids.cur_ex_tx.text = str(len(current_tx))
if sorted(prev_tx) != sorted(current_tx):
content.ids.prev_ex_tx.color = self._red
content.ids.cur_ex_tx.color = self._red
self.show_popup(title="", content=content, size=(520, 400),
separator_color=(0, 0, 0, 0))
[docs] def orto_show_plot(self, active_group, plt_idx, filt=None,
exclude_taxa=None):
"""
Loads a orto_plot screen for orthology graphical exploration based
on the plot index. This method can be called in three ways:
..: Orthology Explore screen button, which generates the plot for
the first time and sets initial attributes for plot screen
header. This only uses the plt_idx argument.
..: Update button in plot screen, used after gene/species filters
have been changed. This uses the plt_idx and filt arguments.
..: Taxa filter button in plot screen, used after changing the taxa
that should be included in the plot analysis. This uses the
plt_idx and exclude_taxa arguments
:param active_group: Group Object.
:param plt_idx: string, id of the plot in plt_method to issue the
appropriate method
:param filt: list, contains the gn and sp filters for group object,
respectively. If none, this will set a new plot and all plot screen
attributes. Else, it will only update the plot image and ortholog
statistics.
:param exclude_taxa: list, each element should be a taxon name to be
excluded from the plot
"""
# Set active group
if active_group:
self.active_group = active_group
else:
active_group = self.active_group
# Exclude taxa, if any
if exclude_taxa:
# If all taxa were excluded issue a warning and do nothing more
if set(exclude_taxa) == set(self.active_group.species_list):
return self.dialog_floatcheck(
"At least one taxon must be included.",
t="warning")
# Setting filters for the first time
if not filt and not exclude_taxa:
self.screen.ids.gn_spin.value = self.active_group.max_extra_copy
self.screen.ids.sp_spin.value = 1
self.screen.ids.header_content.original_filt = \
[self.active_group.max_extra_copy, 1]
self.screen.ids.header_content.root_filt = \
[active_group.gene_threshold, active_group.species_threshold]
self.screen.ids.header_content.root_excluded = \
active_group.excluded_taxa
self.active_group.exclude_taxa([])
self.active_group.update_filters(
self.active_group.max_extra_copy, 1, True)
self.run_in_background(
get_orto_data,
self.orto_write_plot,
[active_group, plt_idx, filt, exclude_taxa],
[plt_idx]
)
[docs] def orto_write_plot(self, plot_data, plt_idx):
"""
Provide with the data structure and a plt_idx string identifier, this
function will create the plot file and update the plot screen.
:param plot_data: list/np array, data structure to be used in plot
construction
:param plt_idx: string, identification string of the plot.
"""
if not plot_data["data"]:
self.screen.ids.plot_content.children[0].clear_widgets()
return self.dialog_floatcheck("There is not data to plot with"
"the current filters", t="error")
# Update excluded taxa attribute
self.screen.ids.header_content.excluded_taxa = \
self.active_group.excluded_taxa
# Update orthology filter attributes
self.screen.ids.header_content.original_filt = \
[self.active_group.gene_threshold,
self.active_group.species_threshold]
# Update orthology filter max values
self.screen.ids.gn_spin.max = self.active_group.max_extra_copy
self.screen.ids.sp_spin.max = len(self.active_group.species_list)
# Update slider values if they are outside bounds
if self.screen.ids.gn_spin.value > self.screen.ids.gn_spin.max:
self.screen.ids.gn_spin.value = self.screen.ids.gn_spin.max
if (self.screen.ids.sp_spin.value >
len(self.active_group.species_list)):
self.screen.ids.sp_spin.value = \
len(self.active_group.species_list)
# Set the current plt_idx for update reference
self.screen.ids.header_content.plt_idx = plt_idx
# Update summary attributes
self.screen.ids.orto_sum.text = "[size=26][color=71c837ff]%s" \
"[/color][/size][size=13]/[color=ff5555ff]%s[/color]" \
"[/size]" % \
(str(self.active_group.all_compliant),
str(self.active_group.all_clusters))
self.screen.ids.taxa_sum.text = "[size=26][color=71c837ff]%s" \
"[/color][/size][size=13]/[color=ff5555ff]%s[/color]" \
"[/size]" % \
(len(self.active_group.species_list),
len(self.active_group.excluded_taxa) +
len(self.active_group.species_list))
# Call corresponding method and catch plot object
self.current_plot, self.current_lgd, self.current_table = \
self.orto_plt_method[plt_idx][0](**plot_data)
# Get plot path
plot_path = join(self.temp_dir, self.orto_plt_method[plt_idx][1])
# Save plot figure
if self.current_lgd:
self.current_plot.savefig(
plot_path,
bbox_extra_artists=(self.current_lgd,),
bbox_inches="tight", dpi=200)
else:
self.current_plot.savefig(
plot_path,
bbox_inches="tight", dpi=200)
# Load plot
self.load_plot(plot_path,
self.screen.ids.plot_content)
@staticmethod
[docs] def load_plot(file_path, scatter_wgt):
"""
Loads a new plot into a ScatterLayout. This will clear all previous
content and load a new image based on the file_path argument.
This assumes that the current screen is a plot related screen.
:param file_path: string. Path to the image to be loaded
:param scatter_wgt: ScatterLayout object, where the plot is to be
loaded
:return:
"""
# Clear previous content
scatter_wgt.children[0].clear_widgets()
# Add content
img_wgt = Image(source=file_path, nocache=True)
scatter_wgt.children[0].add_widget(img_wgt)
# Reset position and scale of Scatter
scatter_wgt.scale = 1
scatter_wgt.pos = (0, 0)
[docs] def dialog_set_exclude_orto_taxa(self):
"""
Creates a dialog for the orthology change filters option.
"""
def set_excluded():
excluded_taxa = [x.text for x in
self._subpopup.content.ids.rev_inlist.children if
x.state == "normal"]
if len(excluded_taxa) == \
len(self._subpopup.content.ids.rev_inlist.children):
return self.dialog_floatcheck("At least one taxon must be "
"included.", t="error")
self._popup.content.excluded_tx = excluded_taxa
self._popup.content.ids.exclude_bt.text = "{} included".format(
len(sp_list) + len(ex_list) - len(excluded_taxa))
self.dismiss_subpopup()
sp_list = self.ortho_groups.taxa_list[self.active_group_name]
ex_list = self.ortho_groups.excluded_taxa[self.active_group_name]
content = InputList(cancel=self.dismiss_subpopup)
# Show the select/deselect all buttons
content.ids.selection_bx.height = 50
# Add button for each taxon
for taxon in sorted(sp_list + ex_list):
bt = TGToggleButton(text=taxon, height=30, state="down")
# deselect button if taxa is excluded
if self._popup.content.excluded_tx is not None:
if taxon in self._popup.content.excluded_tx:
bt.state = "normal"
elif taxon in ex_list:
bt.state = "normal"
# Add button to list
content.ids.rev_inlist.add_widget(bt)
content.ids.ok_bt.bind(on_release=lambda x: set_excluded())
self.show_popup(title="Included taxa", content=content,
size_hint=(.3, .8), popup_level=2)
[docs] def dialog_exclude_orto_taxa(self, plt_idx):
content = InputList(cancel=self.dismiss_popup)
# Show the select/deselect all buttons
content.ids.selection_bx.height = 50
# Add button for each taxon
for taxon in sorted(self.active_group.species_list +
self.active_group.excluded_taxa):
bt = TGToggleButton(text=taxon, height=30, state="down")
# deselect button if taxa is excluded
if taxon in self.active_group.excluded_taxa:
bt.state = "normal"
# Add button to list
content.ids.rev_inlist.add_widget(bt)
# Add bindings to Ok button
content.ids.ok_bt.bind(on_release=lambda j:
self.run_in_background(
get_active_group,
self.orto_show_plot,
[self.ortho_groups, self.active_group,
str(self.active_group_name)],
[str(plt_idx), [int(self.screen.ids.gn_spin.value),
int(self.screen.ids.sp_spin.value)], [x.text for x in
content.ids.rev_inlist.children if x.state == "normal"]],
False,
cancel=True))
self.show_popup(title="Included taxa", content=content,
size_hint=(.3, .8))
[docs] def orto_change_state(self):
"""
Toggle selection or deselection of group checkboxes
"""
# In case all are already selected, deselect all
if len(self.screen.ids.group_check.children) == \
len([x for x in self.screen.ids.group_check.children if
x.active]):
for chk in self.screen.ids.group_check.children:
chk.active = False
else:
for chk in self.screen.ids.group_check.children:
chk.active = True
[docs] def orto_check_state(self):
"""
sets the "Compare" button disabled attribute according to the
number of active check boxes
"""
# Change Compare button state
if (len([x for x in self.screen.ids.group_check.children if
x.active]) >= 2):
self.screen.ids.compare_group_bt.disabled = False
else:
self.screen.ids.compare_group_bt.disabled = True
# Check master check box state
if all([x.active for x in self.screen.ids.group_check.children]):
self.screen.ids.master_chk.active = True
else:
self.screen.ids.master_chk.active = False
[docs] def load_groups(self, groups_obj, default_filters):
"""
Loads the group files generated by the Orthology search or manually
imported into the app. This method only accepts MultiGroup objects.
Loading groups from files is more computationally intensive and
should be done in the background using the run_in_background and
load_group_files methods.
:param groups_obj: MultiGroup object
:param default_filters: Tuple. With filters for MultiGroups object
"""
if groups_obj:
if self.ortho_groups:
self.ortho_groups.add_multigroups(groups_obj)
new_groups = [x for x in groups_obj.groups if
x not in self.ortho_groups.duplicate_groups]
else:
self.ortho_groups = groups_obj
self.ortho_groups.filters = default_filters
new_groups = self.ortho_groups.groups.keys()
self.ortho_group_files.extend(list(groups_obj.groups.keys()))
# Check if any group file is duplicate. If so, issue a warning
if groups_obj.duplicate_groups or groups_obj.bad_groups:
msg = ""
if groups_obj.duplicate_groups:
msg += "The following group files were found to be" \
" duplicate and were not loaded:\n\n[b]%s[/b]" %\
"\n".join(basename(x) for x in
groups_obj.duplicate_groups)
# Reset duplicate_groups attribute
self.ortho_groups.duplicate_groups = []
if groups_obj.bad_groups:
msg += "The following group files could not be parsed "\
"as group files:\n\n[b]%s[/b]" % \
"\n".join(basename(x) for x in
groups_obj.bad_groups)
# Reset bad_groups attribute
self.ortho_groups.bad_groups = []
self.dialog_warning("Invalid group files detected", msg)
if groups_obj.groups:
# Removes "No groups loaded" button if it still exists
try:
self.screen.ids.group_check.remove_widget(
self.screen.ids.no_bt)
except ReferenceError:
pass
# Populate the app gridlayout with group buttons
for gname in sorted(groups_obj.groups):
# If group name contains full path, get only file name
gname_short = basename(gname)
if gname not in self.ortho_groups.duplicate_groups:
# Create check box for multiple group selection
chk = CheckBox(id=gname, size_hint=(.1, None),
height=30)
chk.bind(active=lambda i, y:
self.orto_check_state())
self.screen.ids.group_check.add_widget(chk)
# Create group button
bt = ToggleButton(text=gname_short, id=gname,
group="group_bts", size_hint_y=None,
height=30, shorten=True, shorten_from="right",
halign="center", bold=True,
background_down=join("data", "backgrounds",
"bt_process.png"),
background_normal=join("data", "backgrounds",
"bt_process_off.png"),
background_disabled_down=join("data",
"backgrounds",
"bt_process.png"),
disabled_color=(1, 1, 1, 1))
# Apparently I need to use partial instead of lambda
# in order to provide a diferent group object as
# argument. Using lambda will overwrite the group
# objects of all buttons with the last group of the
# iteration.
bt.bind(on_release=partial(self.orthology_card,
gname))
# Add box to gridlayout
self.screen.ids.group_gl.add_widget(bt)
# Create removal button
x_bt = Button(size_hint=(None, None), width=30,
height=30, id=gname, border=(0, 0, 0, 0),
background_normal=join("data", "backgrounds",
"remove_bt.png"),
background_down=join("data", "backgrounds",
"remove_bt_down.png"))
x_bt.bind(on_release=partial(
self.check_action,
"Are you sure you want to remove this group?",
self.remove_groups))
self.screen.ids.group_rm.add_widget(x_bt)
# If last group name contains a directory, set it as the
# default export dir
try:
path = dirname(gname)
if os.path.exists(path):
self.orto_export_dir = path
except AttributeError:
pass
self.run_in_background(
orto_update_filters,
self.orthology_card,
[self.ortho_groups, None, None, [],
new_groups, True],
None,
False,
msg="Setting up filters...",
cancel=False)
[docs] def orthology_card(self, group_name=None, bt=None):
"""
Generates the descriptive cards with general information for a group
file.
:param group_name
:param bt: ToggleButton instance
"""
# If no group button is active, dispatch the first
# The comparison is done with __class__.__name__ to avoid
# issues of the isinstance() method comparing the same class
# instance from different imports (trifusion.ortho vs. ortho)
if group_name and group_name.__class__.__name__ == "MultiGroupsLight":
try:
self.ortho_groups = group_name
except:
pass
if group_name and not \
group_name.__class__.__name__ == "MultiGroupsLight":
pass
elif (not [x for x in self.screen.ids.group_gl.children
if x.state == "down"] and
self.screen.ids.group_gl.children):
self.screen.ids.group_gl.children[-1].state = "down"
self.screen.ids.group_gl.children[-1].disabled = True
group_name = self.screen.ids.group_gl.children[-1].id
else:
group_name = [x.id for x in self.screen.ids.group_gl.children
if x.state == "down"][0]
self.active_group_name = group_name
# Create desired behaviour for group toggle buttons
if bt:
self.toggle_groups(bt)
# Get statistics from group object
stats = self.ortho_groups.groups_stats[group_name]["stats"]
# Create cards
cards = DescriptionBox(opacity=0)
# Populate card with group information
cards.prot_txt = str(stats[1])
cards.ortholog_txt = str(stats[0])
cards.taxa_txt = str(len(self.ortho_groups.groups_stats[group_name]
["species"]))
cards.group_name = basename(group_name)
# Create gauge plots, if there are any filtered groups
if self.ortho_groups.filters[group_name][0] or \
self.ortho_groups.filters[group_name][1] or \
self.ortho_groups.filters[group_name] == (0, 0):
# Create species filter plot and add to box
sp_filter_plot = GaugePlot()
sp_filter_plot.txt = "After species filter"
sp_filter_plot.proportion = float(stats[3]) / float(stats[0])
sp_filter_plot.ortholog_num = "{0:,}".format(stats[3])
cards.ids.gauge_bx.add_widget(sp_filter_plot)
# Create gene filter plot and add to box
gn_filter_plot = GaugePlot()
gn_filter_plot.txt = "After gene filter"
gn_filter_plot.proportion = float(stats[2]) / float(stats[0])
gn_filter_plot.ortholog_num = "{0:,}".format(stats[2])
cards.ids.gauge_bx.add_widget(gn_filter_plot)
# Create final ortholog plot
final_ortholog_plot = GaugePlot()
final_ortholog_plot.txt = "Final orthologs"
final_ortholog_plot.proportion = float(stats[4]) / \
float(stats[0])
final_ortholog_plot.ortholog_num = "{0:,}".format(stats[4])
cards.ids.gauge_bx.add_widget(final_ortholog_plot)
# Add button to generate full report
full_rep_bt = Button(text="Generate full report", bold=True,
background_normal=join("data", "backgrounds",
"check_ok.png"),
background_down=join("data", "backgrounds",
"check_cancel.png"),
size=(180, 40), size_hint=(None, None),
font_size=17)
full_rep_bt.bind(on_release=lambda i: self.orto_report_dialog())
# Anchor layout that will hold full report button
anc = AnchorLayout(anchor_y="center", achor_x="center",
size_hint_y=None, height=100)
anc.add_widget(full_rep_bt)
cards.add_widget(anc)
self.screen.ids.card_gl.clear_widgets()
# Add card
Clock.schedule_once(
lambda y: self.screen.ids.card_gl.add_widget(cards), .3)
Clock.schedule_once(
lambda y: Animation(opacity=1, d=.3, t="out_quart").start(
cards), .3)
# ########################## POPUP OPS #################################
[docs] def dismiss_close_float(self, *args):
"""
Use to automatically remove any possibly CloseFloat widgets from the
root window, when a Popup is automatically dismissed
"""
try:
rm_wgt = [x for x in self.root_window.children if
isinstance(x, CloseFloat)][0]
self.root_window.remove_widget(rm_wgt)
except IndexError:
pass
[docs] def dismiss_exit(self, *args):
"""
Dismiss function for exit popup
"""
if self._exit_popup:
self._exit_popup.dismiss()
# ########################## PROCESS SCREEN ############################
[docs] def save_file(self, path, file_name=None, idx=None, auto_close=True,
suffix=""):
"""
Adds functionality to the save button in the output file chooser. It
gathers information on the specified path through filechooser, file
name through textinput and the widget text when called.
For now, only one main output file can be provided, so the its path
is stored in a string attribute.
:param path: string. complete path
:param file_name: string. file name only
:param idx: string. An id of where the filechooser is calling. This
allows the addition of custom behaviours for different dialogs
:param auto_close: Boolean. When True, the dismiss_popup method is called in
the end of the function
:param suffix: string. Allow the addition of a suffix to the file name
or to be save in an attribute
"""
if idx == "main_output":
if self.main_operations["concatenation"]:
# Adds output file to storage
try:
self.output_file = join(unicode(path, "utf8"),
file_name)
except TypeError:
self.output_file = join(path, file_name)
self.output_dir = path
# Renames the output file button text
self.process_grid_wgt.ids.conv.text = file_name
else:
try:
self.output_dir = unicode(path, "utf8")
except TypeError:
self.output_dir = path
self.process_grid_wgt.ids.conv.text = basename(path)
self.conversion_suffix = suffix
elif idx == "ortho_dir":
self.ortho_dir = path
self.screen.ids.orto_dir.text = basename(path)
elif idx == "protein_db":
self.protein_db = path[0]
elif idx == "cds_db":
self.cds_db = path
elif idx == "orto_export_dir":
self.orto_export_dir = path
elif idx == "ima2_popfile":
self.ima2_options[0] = path
elif idx == "zorro_dir":
self.zorro_dir = path
elif idx == "mcl_fix":
# The mcl executable requires the .exe extension in windows to
# work properly
if sys.platform in ["win32", "cygwin"]:
mcl_string = "mcl.exe"
else:
mcl_string = "mcl"
# Copy new mcl_file to app_dir
if self._check_exec(path, "mcl"):
# Copy mcl executable to app dir
shutil.copyfile(path, join(self.user_data_dir, mcl_string))
self.mcl_file = join(self.user_data_dir, mcl_string)
# Make it executable
st = os.stat(self.mcl_file)
os.chmod(self.mcl_file, st.st_mode | stat.S_IEXEC)
self.dismiss_all_popups()
# Set the orthology fields to green
self.ortho_search_options.ids.mcl_check.background_src = \
"data/backgrounds/green_noise.png"
self.ortho_search_options.ids.mcl_check_ico.icon_src = \
"data/backgrounds/check_icon.png"
self.ortho_search_options.ids.mcl_check_ico.icon_size = \
(20, 16)
self.ortho_search_options.ids.mcl_check_lbl.text = \
"MCL is installed and reachable"
self.ortho_search_options.ids.mcl_fix_box.clear_widgets()
else:
self.dialog_floatcheck("The provided MCL executable does "
"not seem to be correct.", t="error")
elif idx == "usearch_fix":
# Copy new usearch_file to app_dir
shutil.copyfile(path, join(self.user_data_dir, "usearch"))
self.usearch_file = join(self.user_data_dir, "usearch")
# Make it executable
st = os.stat(self.usearch_file)
os.chmod(self.usearch_file, st.st_mode | stat.S_IEXEC)
if self._check_exec(self.usearch_file, "usearch"):
# Copy mcl executable to app dir
self.dismiss_all_popups()
# Set the orthology fields to green
self.ortho_search_options.ids.usearch_check.background_src =\
"data/backgrounds/green_noise.png"
self.ortho_search_options.ids.usearch_check_ico.icon_src = \
"data/backgrounds/check_icon.png"
self.ortho_search_options.ids.usearch_check_ico.icon_size = \
(20, 16)
self.ortho_search_options.ids.usearch_check_lbl.text = \
"USEARCH is installed and reachable"
self.ortho_search_options.ids.usearch_fix.clear_widgets()
else:
os.remove(self.usearch_file)
self.usearch_file = ""
self.dialog_floatcheck("The provided USEARCH executable"
" does "
"not seem to be correct.", t="error")
if auto_close:
self.dismiss_popup()
[docs] def save_gapfilter(self, filter_act, within_chk, among_chk, gap_val,
mis_val, min_tx_val):
"""
Stores the information of the FilterDialog
:param filter_act: Boolean, whether the filter is active (True) or
not (False)
:param within_chk: Boolean, whether the within alignment filter
is active (True) or not (False)
:param among_chk: Boolean, whether the multiple alignments filter
is active (True) or not (False)
:param gap_val: integer, proportion of gap threshold
:param mis_val: integer, proportion of missing data threshold
:param min_tx_val: integer, proportion of minimum taxa
representation
"""
if filter_act and not within_chk and not among_chk:
return self.dialog_floatcheck("Warning: No filters were "
"checked. Specify at least one to active the filter.",
t="error")
self.secondary_options["gap_filter"] = filter_act
# Save only when the filter is set to active
if filter_act:
self.missing_filter_settings = [(within_chk, gap_val, mis_val),
(among_chk, min_tx_val)]
self.dismiss_popup()
[docs] def save_taxafilter(self, filter_act, filter_mode, taxa_group):
"""
Stores the information of the taxa filter dialog of the process
screen
:param filter_mode: string, determines the taxa filtering mode. Can
be either 'Contain' or 'Exclude'
:param taxa_group: string, with the name of the taxa group that
should be present in the taxa_group attribute
"""
# Check if a taxa group has been selected. If not issue a warning
# and do not close popup
if taxa_group == "No group selected":
return self.dialog_floatcheck("Warning: A taxa group must be "
"specified to activate the filter", t="error")
self.secondary_options["taxa_filter"] = filter_act
# Save only when the filter is set to active
if filter_act:
self.taxa_filter_settings = [filter_mode, taxa_group]
self.dismiss_all_popups()
[docs] def save_codonfilter(self, filter_act, position_list):
"""
Stores the information of the alignment filter dialog of the process
screen
:param filter_act: Boolean, whether the filter is active (True) or
not (False)
:param position_list: A list of three elements, containing which
positions should be saved (True) or filtered (False)
"""
self.secondary_options["codon_filter"] = filter_act
if filter_act:
self.codon_filter_settings = position_list
self.dismiss_popup()
[docs] def save_variationfilter(self, filter_act, *pargs):
"""
Stores the information of the variation filter dialog.
:param filter_act: Boolean, whether the filter is active (True) or
not (False)
:param pargs, list of tuples, with first element being the
checkbox active property and the second element the value of said
parameter
"""
# Check if any parameter was checked. If not, issue warning and
# do not close popup
if not any([p[0] for p in pargs]) and filter_act:
return self.dialog_floatcheck("Warning: No parameters were "
"checked. Specify at least one to activate the filter.",
t="error")
# Check if all checked parameters have values. If not,
# issue warning and do not close popup
for p in pargs:
if p[0] and not p[1]:
return self.dialog_floatcheck("Warning: Checked "
"parameters must have values specified.", t="error")
# Check for consistency between min and max values
for i in range(0, len(pargs), 2):
try:
vals = (int(pargs[i][1]), int(pargs[i + 1][1]))
if vals[0] and vals[1] and vals[0] > vals[1]:
return self.dialog_floatcheck("Warning: Minimum values "
"cannot be greater than maximum values", t="error")
except ValueError:
pass
self.secondary_options["variation_filter"] = filter_act
if filter_act:
for i, p in enumerate(pargs):
p = int(p[1]) if p[0] else None
self.variation_filter[i] = p
self.dismiss_popup()
[docs] def save_ima2_opts(self, pop_string, mutation, inheritance):
"""
Check each text input parameter given by the user and, if they
checkout, saves them.
:param pop_string: string, with population tree
:param mutation: string, with mutation model
:param inheritance: string, with inheritance scalar
"""
for i, msg in zip([self.ima2_options[0], pop_string, mutation,
inheritance],
["population file",
"population tree string",
"mutation model",
"inheritance scalar"]):
if not i:
return self.dialog_floatcheck("Please specify a "
"{}".format(msg), t="warning")
self.ima2_options[1] = pop_string
self.ima2_options[2] = mutation
self.ima2_options[3] = inheritance
self.dismiss_all_popups()
[docs] def save_zorro_settings(self, suffix):
"""
Handles the information provided by the user in the ZorroDialog
:param suffix: string, suffix of the ZORRO files
"""
# Only perform file checking is switch is active
if self._popup.content.ids.zorro_switch.active:
# Check if the zorro files exist
for f in self.active_file_list:
# Check if a specific directory for the ZORRO files has been
# specified. If so, get only the file name of the
# active_file_list and use the zorro_dir to provide the path
if self.zorro_dir:
f = splitext(f.split(sep)[-1])[0]
f = "{}{}.txt".format(join(self.zorro_dir, f), suffix)
# If zorro_dir is not specified, assume that the ZORRO files
# are in he same working directory of the input files
else:
f = sep.join(f.split(".")[0:-1])
f = "%s%s.txt" % (f, suffix)
if not os.path.isfile(f):
return self.dialog_floatcheck(
"File %s does not exist" % f, t="error")
self.update_process_switch(
"zorro", self._popup.content.ids.zorro_switch.active)
if self.secondary_options["zorro"]:
# Save auxiliary files suffix
self.zorro_suffix = suffix
# Change background and text of the Zorro button in process
# options
self.process_options.ids.zorro.background_normal = \
"data/backgrounds/bt_process.png"
self.process_options.ids.zorro.text = "Active"
else:
self.process_options.ids.zorro.background_normal = \
"data/backgrounds/bt_process_off.png"
self.process_options.ids.zorro.text = "OFF"
self.dismiss_popup()
[docs] def save_reverseconc_partfile(self, partfile):
"""
Save partition file for reverse concatenation an update button in
reverse concatenation dialog
:param partfile: string. Path to partition file
"""
self.dismiss_subpopup()
self.partitions_file = partfile
# Update partition file button
self._popup.content.ids.part_file.text = basename(partfile)
self._popup.content.ids.part_file.background_normal = \
"data/backgrounds/bt_process.png"
# Deactivate user defined partitions
self._popup.content.ids.use_parts.state = "normal"
self.use_app_partitions = False
# Change background of used defined partition button
self._popup.content.ids.use_parts.state = "normal"
[docs] def dialog_floatcheck(self, text, t, dl=5):
"""
Creates a floating label with informative text on the right upper
corner of the app. This is used for showing errors, warnings and
general informative messages that fade in and fade out after a time
:param text: string, text to appear in the label
:param t: string, with type of check. Can be either error or info
:param dl: integer, duration of the float
"""
def fade_in():
Animation(opacity=1, d=.5, t="out_quart").start(check_wgt)
Animation(opacity=1, d=.5, t="out_quart").start(rm_wgt)
Animation(opacity=1, d=.5, t="out_quart").start(ico_wgt)
def fade_out():
# Get height of warning
h = check_wgt.height
pos_h = check_wgt.pos[1]
for wgt in [check_wgt, rm_wgt, ico_wgt]:
Animation(opacity=0, d=.2, t="out_quart").start(wgt)
Clock.schedule_once(
lambda dt: self.root_window.remove_widget(check_wgt), .2)
Clock.schedule_once(
lambda dt: self.root_window.remove_widget(rm_wgt), .2)
Clock.schedule_once(
lambda dt: self.root_window.remove_widget(ico_wgt), .2)
# Check if any warnings are still present. If so, update vertical
# position
current_floats = [i for i in self.root_window.children
if isinstance(i, WarningFloat) or
isinstance(i, RemoveFloat) or
isinstance(i, IconFloat)]
if len(current_floats) > 1:
for wgt in current_floats:
if pos_h > wgt.pos[1]:
pos = (wgt.pos[0], wgt.pos[1] + 15 + h)
Animation(pos=pos, t="out_quart").start(wgt)
# The spacing variable will be updated depending on the number of
# warning floats present in the root window.
spacing = 0
# Check if float is already present.
try:
wf = [x for x in self.root_window.children
if isinstance(x, WarningFloat)]
# If a warning with the same text is already present, ignore
if [x for x in wf if x.text == text]:
return
# Else, update the spacing according to the number of floats
# present
spacing += sum([x.height + 15 for x in wf])
except IndexError:
pass
# Get position from root window
x, y = self.root_window.size
# Create widget
check_wgt = WarningFloat(text=text, opacity=0, markup=True)
check_wgt.texture_update()
check_wgt.pos = x - check_wgt.size[0] - 20, \
y - check_wgt.size[1] - 60 - spacing
# Create remove button
rm_wgt = RemoveFloat(pos=(x - 38, y - 75 - spacing), opacity=0)
rm_wgt.bind(on_release=lambda arg: fade_out())
# Create icon float
ico_wgt = IconFloat(pos=(check_wgt.pos[0] - 20,
check_wgt.pos[1] + (check_wgt.height / 2)
- 10), opacity=0)
# Determine background color
if t in ["error", "warning"]:
check_wgt.background_src = "data/backgrounds/red_noise.png"
ico_wgt.icon_src = "data/backgrounds/warning_icon.png"
else:
check_wgt.background_src = "data/backgrounds/green_noise.png"
ico_wgt.icon_src = "data/backgrounds/check_icon.png"
ico_wgt.size = (20, 16)
ico_wgt.pos[1] += 2
# Add widget
self.root_window.add_widget(check_wgt)
self.root_window.add_widget(rm_wgt)
self.root_window.add_widget(ico_wgt)
# Set animations
fade_in()
Clock.schedule_once(lambda arg: fade_out(), dl)
[docs] def check_partitions_file(self):
"""
This will make some checks on the partitions file provided by the
user. It will check for errors in the format of the file itself,
and whether the partitions are correctly defined
"""
aln_obj = self.alignment_list.retrieve_alignment(self.rev_infile)
part_obj = Partitions()
part_obj.set_length(aln_obj.locus_length)
er = part_obj.read_from_file(self.partitions_file)
if not er:
return
elif isinstance(er, Exception):
self.dialog_warning("Invalid partitions file", er.value)
return er
# Check for the validity of the partitions file
# if isinstance(er, InvalidPartitionFile):
# return self.dialog_floatcheck(
# "The provided partitions file is invalid. Please check "
# "the file or replace with an appropriate one.", t="error")
#
# # Check for the validity of the partitions file
# if isinstance(aln_er, InvalidPartitionFile):
# return self.dialog_floatcheck(str(aln_er), t="error")
# else:
# return True
[docs] def save_reverseconc_settings(self, use_parts=False):
"""
Handles the information provided by the LoadDialog with settings
for the reverse concatenation
:param use_parts: boolean. If True, use a partition file. Else
Use user defined partitions
"""
if not self.partitions_file and not self.rev_infile and \
not self.use_app_partitions and \
self.main_operations["reverse_concatenation"]:
return self.dialog_floatcheck("Please provide a partition "
"file and file to reverse concatenate OR use defined "
"partitions defined in side panel.", t="error")
if not self.use_app_partitions:
if not self.partitions_file:
# Check if partition file was selected. If not, return
# warning
return self.dialog_floatcheck(
"Please provide a partitions file.", t="error")
if not self.rev_infile:
# Check if file to reverse concatenate was specified. If
# not, return warning
return self.dialog_floatcheck(
"Please provide a file to reverse concatenate.",
t="error")
# Check for the validity of the partitions file
er = self.check_partitions_file()
# Prevent further action is the partitions file is invalid
if er:
return
if self.main_operations["reverse_concatenation"]:
self.screen.ids.rev_conc.background_normal = \
"data/backgrounds/bt_process.png"
self.screen.ids.rev_conc.text = "Active"
else:
self.screen.ids.rev_conc.background_normal = \
"data/backgrounds/bt_process_off.png"
self.screen.ids.rev_conc.text = "OFF"
self.dismiss_popup()
[docs] def dialog_reverse_inlist(self):
content = InputList(cancel=self.dismiss_subpopup)
def set_infile(txt, wgt):
# Update file to reverse concatenate button
self.rev_infile = txt
self._popup.content.ids.rev_infile.background_normal = \
"data/backgrounds/bt_process.png"
self._popup.content.ids.rev_infile.text = basename(txt)
# Deactivate use defined partitions
self._popup.content.ids.use_parts.state = "down"
self.use_app_partitions = False
self.dismiss_subpopup()
if self.file_list:
for infile in self.file_list:
bt = Button(text=basename(infile), size_hint_y=None,
height=30, bold=True, shorten=True,
shorten_from="right", halign="center",
valign="middle")
bt.text_size[0] = bt.size[0] * 3.5
bt.bind(on_release=partial(set_infile, infile))
content.ids.rev_inlist.add_widget(bt)
self.show_popup(title="Choose input file",
content=content,
size_hint=(.5, .8),
popup_level=2)
[docs] def dialog_reverse_concatenation(self, title="Choose input file"):
"""
Generates a general purpose file chooser to request additional data
:param title: string, A custom title for the load data dialog popup
"""
content = RevConcDialog(cancel=self.dismiss_popup)
content.ids.rev_conc.active = \
self.main_operations["reverse_concatenation"]
# Check if use app partitions button has been selected
if self.use_app_partitions:
content.ids.use_parts.state = "down"
# Check if partitions file was already selected. If so, update the
# corresponding button
if self.partitions_file:
content.ids.part_file.background_normal = \
"data/backgrounds/bt_process.png"
content.ids.part_file.text = basename(self.partitions_file)
content.ids.defined.collapse = True
content.ids.manual.collapse = False
# Check if input file to reverse concatenate was already selected.
# If so, update the corresponding button
if self.rev_infile:
content.ids.rev_infile.background_normal = \
"data/backgrounds/bt_process.png"
content.ids.rev_infile.text = basename(self.rev_infile)
self.show_popup(title=title, content=content, size=(450, 360))
if self.use_app_partitions:
content.ids.defined.collapse = False
content.ids.manual.collapse = True
else:
content.ids.manual.collapse = False
content.ids.defined.collapse = True
[docs] def dialog_load_partfile(self):
content = SaveDialog(cancel=self.dismiss_subpopup,
bookmark_init=self.bookmark_init)
# If input files have already been provided, use their directory
# as a starting point for the partition file chooser. Otherwise,
# use the home path
if self.file_list:
content.ids.sd_filechooser.path = unicode(
dirname(self.file_list[0]))
else:
content.ids.sd_filechooser.path = self.home_path
content.ids.sd_filechooser.text = "partition_file"
content.ids.txt_box.height = 0
content.ids.txt_box.clear_widgets()
self.show_popup(title="Choose partition file",
content=content, size_hint=(.9, .9),
popup_level=2)
[docs] def dialog_filechooser(self, idx=None, popup_level=1):
"""
Generates a file chooser popup for the user to select an output file
:param idx: string. An id of where the filechooser is calling. This
allows the addition of custom behaviours for different dialogs
:param popup_level: int, level of popup. 1 for _popup, 2 for
_subpopup and 3 for exit popup
"""
def update_bw_box(ext_wgt, bw_wgt):
"""
Function used to update the disable status of the black and white
check box depending on the plot extensions that is selected
"""
if ext_wgt.text != ".png":
bw_wgt.ids.bw.disabled = True
bw_wgt.ids.bw.active = False
bw_wgt.ids.lbl.color = (.7, .7, .7, .7)
else:
bw_wgt.ids.bw.disabled = False
bw_wgt.ids.lbl.color = (1, 1, 1, 1)
# Determines cancel action depending on popup_level
cancel = self.dismiss_popup if popup_level == 1 else \
self.dismiss_subpopup
# Lists the idx that require the selection of file extensions
idx_with_ext = ["export_graphic"]
# Lists the idx that do not required file name
idx_no_file = ["ortho_dir", "zorro_dir", "protein_db", "mcl_fix",
"usearch_fix", "cds_db"]
# Maps idx for which an extension label is provided in the filechooser
# The key is the idx, the value is the extension to appear in the label
idx_ext_label = {"export_outliers": ".txt",
"export_table": ".csv",
"export": ".txt"}
# Inherits the layout defined in the .kv file under <SaveDialog>
content = SaveDialog(cancel=cancel,
bookmark_init=self.bookmark_init)
# Maps the filechooser idx to the corresponding title
title_map = {
"main_output":
"Choose destination directory of output file(s)",
"export":
"Choose file for exportation",
"export_table":
"Export as table...",
"export_graphic":
"Export as graphic...",
"zorro_dir":
"Choose directory with ZORRO weight files",
"protein_db":
"Choose protein sequence database file",
"cds_db":
"Choose cds/transcript sequence database file(s)",
"export_outliers":
"Export outliers...",
"ortho_dir":
"Choose destination directory for OrthoMCL output files...",
"mcl_fix":
"Select the MCL executable...",
"usearch_fix":
"Select the USEARCH executable..."
}
# Add extension selection spinner, if idx in idx_with_ext
if idx in idx_with_ext:
ext_spinner = ExtSpinner()
content.ids.txt_box.ids["ext"] = ext_spinner
bw_box = BWCheck()
ext_spinner.bind(text=lambda x, y: update_bw_box(ext_spinner,
bw_box))
content.ids.txt_box.add_widget(ext_spinner)
content.ids.txt_box.add_widget(bw_box)
content.ids.txt_box.ids["bw"] = bw_box
# Remove file name text input if idx in idx_no_file
if idx in idx_no_file:
content.ids.txt_box.clear_widgets()
content.ids.txt_box.height = 0
# Add extension label if idx in idx_ext_label
if idx in idx_ext_label:
ext_label = ExtLabel()
ext_label.text = idx_ext_label[idx]
content.ids.txt_box.add_widget(ext_label)
# Custom behaviour for main output file chooser dialog
if idx == "main_output":
# If the main operation is conversion or reverse concatenation,
# remove the TextInput asking for the file name
if self.main_operations["conversion"] or \
self.main_operations["reverse_concatenation"]:
content.ids.txt_label.text = "Suffix:"
if self.conversion_suffix:
content.ids.text_input.text = self.conversion_suffix
else:
content.ids.text_input.hint_text = \
"Suffix that will be appended to each output file " \
"name. You may leave empty."
else:
title_map["main_output"] = "Choose output file"
# Custom behaviour for orthology output directory
elif idx == "ortho_dir":
if self.ortho_dir:
content.ids.sd_filechooser.path = unicode(self.ortho_dir)
if idx == "cds_db":
content.ids.sd_filechooser.multiselect = True
# Save output file for conversion/concatenation purposes
# Providing this operation will allow the filechooser widget to
# know which output file is this
content.ids.sd_filechooser.text = idx
self.show_popup(title=title_map[idx], content=content,
popup_level=popup_level)
[docs] def dialog_taxafilter(self):
"""
Generates dialog for taxa filter in the additional options of the
process screen
"""
content = TaxaFilterDialog(cancel=self.dismiss_all_popups)
self.show_popup(title="Advanced taxa filter", content=content,
size=(350, 400))
[docs] def dialog_codonfilter(self):
"""
Generates dialog for alignment filter in the additional options of
the process screen
"""
content = CodonFilterDialog(cancel=self.dismiss_popup)
self.show_popup(title="Advanced alignment filter", content=content,
size=(330, 280))
[docs] def dialog_filter(self):
"""
Generates the settings popup for filtering options
"""
content = FilterDialog(cancel=self.dismiss_popup)
# Update filter values if they were changed
if self.missing_filter_settings:
# For within alignment
content.ids.within_chk.active = \
self.missing_filter_settings[0][0]
content.ids.gap_sli.value = self.missing_filter_settings[0][1]
content.ids.mis_sli.value = self.missing_filter_settings[0][2]
# For minimum taxa
content.ids.among_chk.active = \
self.missing_filter_settings[1][0]
content.ids.min_taxa.value = self.missing_filter_settings[1][1]
content.ids.gap_filter.active = self.secondary_options["gap_filter"]
self.show_popup(title="Set filter thresholds", content=content,
size=(400, 490))
[docs] def dialog_variationfilter(self):
"""
Generates the settings popup for variation filter options
"""
p_list = [("var_min", "var_min_val"), ("var_max", "var_max_val"),
("inf_min", "inf_min_val"), ("inf_max", "inf_max_val")]
content = VariationFilterDialog(cancel=self.dismiss_popup)
content.ids.variation_filter.active = self.secondary_options[
"variation_filter"]
# Populate content with previous filters
for i, p in enumerate(self.variation_filter):
content.ids[p_list[i][0]].active = True if p else False
content.ids[p_list[i][1]].text = str(p) if p else ""
self.show_popup(title="Set variation filter options",
content=content, size=(250, 440))
[docs] def dialog_filter_report(self, filtered_stats, nalns):
"""
Generates graphic report at the end of a processing operation
with active filters
:param filtered_stats: Dictionary with the values of the filtered
alignments for each filter type
:param nalns: integer, total number of processed alignments
"""
content = FilterGraphicReport(cancel=self.dismiss_popup)
# Set total number of alignments
content.ids.total_alns.text = str(nalns)
# Set final alignments
final_alns = sum([x for x in filtered_stats.values() if x])
content.ids.final_box.size_hint_x = float(final_alns) / float(nalns)
content.bar_color = (.44, .78, .22, 1)
if final_alns:
content.ids.final_txt.text = str(final_alns)
else:
content.ids.final_box.size_hint_x = 0.00001
content.ids.final_txt2.text = "0"
# Set graphics for each filter
for f, val in filtered_stats.items():
filt_wgt = IndividualFilterWgt()
filt_wgt.ids.main_lbl.text = f
if val:
filt_wgt.ids.gf_box.size_hint_x = float(val) / float(nalns)
filt_wgt.ids.gf_txt.text = str(val)
elif val == 0:
filt_wgt.ids.gf_box.size_hint_x = 0.00001
filt_wgt.ids.gf_txt2.text = "0"
else:
filt_wgt.ids.gf_box.size_hint_x = 0.00001
filt_wgt.ids.gf_txt2.text = "Not applied"
content.ids.filter_bx.add_widget(filt_wgt)
self.show_popup("Filter report", content=content, size=(400, 520))
@staticmethod
[docs] def check_variation_filters(value):
"""
Same function as check_filters method, but checks only for
integer compliance
:param value: text_input.text
"""
x = value.replace(",", ".")
try:
x = int(x)
except ValueError:
# Check if value can be converted to float by removing
# all non digits. To avoid problems like converting 2.23
# to 223, the first step is to get the first value before
# a ".", if any
try:
all_d = string.maketrans("", "")
nodigs = all_d.translate(all_d, string.digits)
x = x.encode("ascii", "ignore")
x = int(x.translate(all_d, nodigs))
except ValueError:
return False
return True, abs(x)
@staticmethod
[docs] def check_filters(value):
"""
Method that validates the input of the text input in filter
settings. It handles common mistakes, such as using "," instead
of "." for decimal places and truncates values between the range
of 0 and 100. If the text input cannot be converted to float,
it will return false and the slider value will not change
:param value: text_input.text
"""
try:
# Check if value can be converted to float just by replacing
# "," to "."
x = float(value.replace(",", "."))
except ValueError:
try:
# Check if value can be converted to float by removing
# all non digits. To avoid problems like converting 2.23
# to 223, the first step is to get the first value before
# a ".", if any
x = value.split(".")[0]
all_d = string.maketrans("", "")
nodigs = all_d.translate(all_d, string.digits)
x = x.encode("ascii", "ignore")
x = float(x.translate(all_d, nodigs))
except ValueError:
return False
if x > 100:
corrected_val = 100
elif x < 0:
corrected_val = 0
else:
corrected_val = int(x)
return True, corrected_val
@staticmethod
[docs] def check_partition_split(value, prange):
"""
Method that validates the text input for manual split of partitions.
Ensures that the input can be converted to int, and whether it is
inside the available range.
"""
try:
# Check if value can be converted to int
x = int(value)
except ValueError:
try:
# Check if value can be converted to int by removing
# all non digits.
x = value
all_d = string.maketrans("", "")
nodigs = all_d.translate(all_d, string.digits)
x = x.encode("ascii", "ignore")
x = int(x.translate(all_d, nodigs))
except ValueError:
return False
if x > prange[1]:
corrected_val = prange[1]
elif x < prange[0]:
corrected_val = prange[0]
else:
corrected_val = x
return True, corrected_val
[docs] def dialog_execution(self):
"""
Generates the dialog for Process execution. It also preforms several
sanity checks before issuing the dialog
"""
if not self.active_file_list:
return self.dialog_floatcheck(
"No input files were loaded or are active",
t="error")
content = ExecutionDialog(cancel=self.dismiss_popup)
# Get number of files to be processed
fs_name = self.process_grid_wgt.ids.active_file_set.text
# Get total number of files loaded
if fs_name == "All files":
file_num = len(self.file_list)
# Get active files
elif fs_name == "Active files":
file_num = len(self.active_file_list)
# Get files from custom file set
else:
file_num = len(self.file_groups[fs_name])
# Perform pre-execution checks
# Get main operation
try:
main_op = [nm for nm, bl in self.main_operations.items()
if bl is True][0]
content.ids.main_op.text = "[b][size=18][color=37abc8ff]Main " \
"operation:[/color][/size][/b] %s" % main_op
except IndexError:
return self.dialog_floatcheck(
"Please select a main operation", t="error")
# Get secondary operations
secondary_op = [nm for nm, bl in self.secondary_operations.items()
if bl is True]
if secondary_op:
content.ids.sec_op.text = "[b][size=18][color=37abc8ff]" \
"Secondary operation(s):[/color][/size][/b] %s" %\
", ".join(secondary_op)
else:
content.ids.sec_op.text = "[b][size=18][color=37abc8ff]" \
"Secondary operation(s):[/color][/size][/b] None"
# Get output formats
content.ids.out_form.text = "[b][size=18][color=37abc8ff]Output " \
"format(s):[/color][/size][/b] %s" % ", ".join(
self.output_formats)
# Get output files
# In case concatenation
if main_op == "concatenation":
# Check if an output directory has been selected
if self.output_file == "":
return self.dialog_floatcheck(
"No output file has been selected", t="error")
out_file = basename(self.output_file)
add_files = [out_file + "_" + nm for nm, bl in
self.secondary_operations.items() if bl and
self.secondary_options["%s_file" % nm]]
content.ids.out_files.text = "[b][size=18][color=37abc8ff]" \
"Output file(s):[/color][/size][/b] (%s) %s, %s" % \
(len(add_files) + 1, out_file, ", ".join(add_files))
# In case conversion
if main_op == "conversion":
# Check if an output file has been selected
if self.output_dir == "":
return self.dialog_floatcheck(
"No output directory has been selected",
t="error")
try:
# Check for additional files
add_files = [nm for nm, bl in self.secondary_operations.items()
if bl and self.secondary_options["%s_file" % nm]]
content.ids.out_files.text = "[b][size=18][color=37abc8ff]"\
"Output file(s):[/color][/size][/b] %s converted " \
"file(s)" % (file_num + file_num * len(add_files))
# In case aln_obj has not being defined, probably because there
# are no input files
except AttributeError:
return self.dialog_floatcheck(
"No input files havebeen selected", t="error")
if main_op == "reverse_concatenation":
if self.output_dir == "":
return self.dialog_floatcheck(
"No output directory has been selected",
t="error")
if main_op == "reverse_concatenation" and not self.rev_infile and \
len(self.file_list) > 1:
return self.dialog_floatcheck(
"Reverse concatenation using partitions defined in "
"the app requires only one input alignment. Please select"
" a single file to reverse concatenate in the Reverse "
"concatenate settings", t="error")
try:
self.show_popup(
title="Process execution summary - Processing %s file(s)" %
file_num,
content=content,
size=(600, 350))
except AttributeError:
return self.dialog_floatcheck(
"No input files have been selected", t="error")
# Check if main operation is reverse concatenation and if the active
# taxa set smaller than the complete set. If so, issue a warning
# that individual partitions that do not contain any of the selected
# taxa will not be written
if main_op == "reverse_concatenation" and self.active_taxa_list != \
self.alignment_list.taxa_names:
self.dialog_warning("Reverse concatenation warning",
"A data set is being reverse "
"concatenated with a taxa subset "
"specified. Individual alignments that"
" do not contain the selected/active"
" taxa will not be written")
[docs] def dialog_text(self, title, idx, msg=None):
"""
Generates a simple text dialog to capture text input
:param title: string. Title of the popup
:param idx: string. Identifier of the dialog
:param msg: string. Option message to appear in the dialog input
text
"""
if idx == "new_folder":
content = TextDialog(cancel=self.dismiss_subpopup)
else:
content = TextDialog(cancel=self.dismiss_popup)
content.text = idx
if idx == "haplotype":
content.ids.txt_dlg.text = self.hap_prefix
elif idx == "usearch_db":
content.ids.txt_dlg.text = self.usearch_db
elif idx == "usearch_out":
content.ids.txt_dlg.text = self.usearch_output
elif idx == "evalue":
content.ids.txt_dlg.text = self.usearch_evalue
elif idx == "orto_group":
content.ids.txt_dlg.text = self.ortholog_prefix
elif idx == "groups":
content.ids.txt_dlg.text = self.group_prefix
elif idx == "change_taxon":
content.ids.txt_dlg.text = msg
content.old_name = msg
elif idx == "project":
# This will generate a default name for the project, based on
# untitled and a number. To avoid duplications, this increases
# the counter c while there are untitled_[0-9] projects
c = 1
with open(self.projects_file, "rb") as project_fh:
project_dic = pickle.load(project_fh)
while "untitled_{}".format(c) in project_dic:
c += 1
content.ids.txt_dlg.text = "untitled_{}".format(c)
content.ids.txt_dlg.select_all()
if idx == "new_folder":
popup_level = 20
else:
popup_level = 1
self.show_popup(title=title, content=content, popup_level=popup_level,
size=(400, 150))
[docs] def dialog_warning(self, msg1, msg2):
content = WarningDialog(cancel=self.dismiss_subpopup)
content.ids.warning_l.text = \
"[b][color=#ff5555ff][size=18]%s[/size][/color][/b]\n\n%s" %\
(msg1, msg2)
self.show_popup(title="[b]Error![/b]", content=content,
size=(550, 300),
separator_color=[255 / 255., 85 / 255., 85 / 255., 1.],
popup_level=2)
[docs] def dialog_zorro(self):
content = ZorroDialog(cancel=self.dismiss_popup)
content.ids.zorro_switch.active = self.secondary_options["zorro"]
content.ids.zorro_txt.text = self.zorro_suffix
self.show_popup(title="ZORRO support", content=content,
size=(400, 260))
[docs] def save_text(self, text, idx):
"""
Saves a text input, whose attribute depends on the idx argument
:param text: string. The text to be assigned to the attribute
:param idx: string. This will determine which attribute will be used
property
"""
if idx == "haplotype":
self.hap_prefix = text
self.process_options.ids.hapbt.text = text
elif idx == "usearch_db":
self.usearch_db = text
elif idx == "usearch_out":
self.usearch_output = text
elif idx == "evalue":
try:
float(text)
self.usearch_evalue = text
except ValueError:
return self.dialog_floatcheck(
"E-value must be a number", t="error")
elif idx == "orto_group":
self.ortholog_prefix = text
elif idx == "groups":
self.group_prefix = text
[docs] def update_main_operations(self, op):
"""
Updates the app attribute containing the main operations of the
Process screen, self.main_operations. Only one main operation can
be active.
:param op: The name of the operation to turn on (all others will be
disabled)
"""
"""
The text of the Output file/directory field changes depending on
whether the main operation is a concatenation or a conversion
"""
file_text = "[size=18][b]Output file[/b][/size]\n[size=13]Save " \
"output file to the selected file.[/size]"
dir_text = "[size=18][b]Output directory[/b][/size]\n[size=13]" \
"Save output file(s) to the selected directory.[/size]"
self.main_operations = {k: True if k == op else False for k in
self.main_operations}
# Disables output file button and other conversion/concatenation
# specific buttons
if op == "conversion":
if self.output_dir == "":
self.process_grid_wgt.ids.conv.text = "Select..."
else:
self.process_grid_wgt.ids.conv.text = \
basename(self.output_dir)
self.process_grid_wgt.ids.output_label.text = dir_text
Animation(height=0, d=.32, t="in_quad").start(
self.screen.ids.sub_conc)
elif op == "concatenation":
if self.output_file == "":
self.process_grid_wgt.ids.conv.text = "Select..."
else:
self.process_grid_wgt.ids.conv.text = \
basename(self.output_file)
self.process_grid_wgt.ids.output_label.text = file_text
Animation(height=50, d=.32, t="in_quad").start(
self.screen.ids.sub_conc)
elif op == "reverse_concatenation":
if self.output_dir == "":
self.process_grid_wgt.ids.conv.text = "Select..."
else:
self.process_grid_wgt.ids.conv.text = \
basename(self.output_dir)
self.process_grid_wgt.ids.output_label.text = dir_text
[docs] def save_main_operation(self, op):
"""
This controls the appearance of the general options after the user
chooses the main operation (Conversion vs. Concatenation). When the
main operation is chosen for the first time, the general options
are introduced, but further selection of the main operation only
changes the state of the operations button.
:param op: string, the main operations. values are "concatenation"
and "conversion"
"""
# If the general options widget is not shown yet, show them
if self.process_grid_wgt.opacity == 0:
# Add to Process screen, via the appropriate scrollview
self.screen.ids.process_sv.add_widget(self.process_grid_wgt)
# Store the original height of the gridlayout containing the
# general options so that it can be updated
self.process_height = self.process_grid_wgt.height
# Animate the appearance of the general options via changes in
# opacity to give a fade in effect
Animation(opacity=1, d=.32, t="in_quad").start(
self.process_grid_wgt)
# Store the active main operation
self.update_main_operations(op)
[docs] def toggle_process_options(self):
"""
Controls the toggling of the GridLayout with the additional options
for the process screen.
"""
# Shows additional options
if self.process_grid_wgt.ids.opt_bt.state == "down":
# Adds widget to the Process widget tree an animates its
# appearance via changes in opacity to give impression of fade
# in
self.process_grid_wgt.add_widget(self.process_options)
Animation(
opacity=1, d=.5, t="in_quad").start(self.process_options)
# Update the height of the GridLayout to allow scrolling
self.process_grid_wgt.height = self.process_height + \
sum([x.height for x in
self.process_options.ids.filter_grid.children]) + 100
# Change text in the toggle button
self.process_grid_wgt.ids.opt_bt.text = \
"Hide additional options"
# Hides additional options
elif self.process_grid_wgt.ids.opt_bt.state == "normal":
# Removes widget from widget tree. However, all settings that
# were change while the widget was visible will be stored
self.process_grid_wgt.remove_widget(self.process_options)
# Change text in the toggle button
self.process_grid_wgt.ids.opt_bt.text = \
"Show additional options"
# ######################### STATISTICS SCREEN ##########################
[docs] def toggle_stats_panel(self, force_close=None, force_open=None):
"""
Controls the animation of the statistics panel
:param force_close: Boolean. If True, close the stats panel
regardless of current position
:param force_open: Boolean. If True, open the stats panel
regardless of current position
"""
expanded_width = 410
if self.screen.ids.stats_panel.width == expanded_width and \
not force_open:
self.sidepanel_animation(width=0,
wgt=self.screen.ids.stats_panel)
self.sidepanel_animation(width=40,
wgt=self.screen.ids.sidepanel_container)
elif self.screen.ids.stats_panel.width == 0 and not force_close:
self.sidepanel_animation(width=expanded_width,
wgt=self.screen.ids.stats_panel)
self.sidepanel_animation(width=expanded_width + 40,
wgt=self.screen.ids.sidepanel_container)
[docs] def toggle_stats_options(self, bt, idx):
"""
Toggles the main data exploration analyses options in the Statistics
screen
"""
def transfer_wgts(source_wgts, sink_gl):
for wgt in source_wgts:
sink_gl.add_widget(wgt)
sink_gl.active_grid = True
bt.disabled = False
bt.disabled = True
# Storage of Options buttons separated by major analysis types
wgts = {
"General information":
[self.screen.ids.general_information, [SizeDistribution(),
NucAAProportions(),
TaxaDistribution()]],
"Missing Data":
[self.screen.ids.missing_data_opts, [GeneOccupancy(),
MissingOrto(),
MissingData(),
CumulativeMissingOrto()]],
"Polymorphism and Variation":
[self.screen.ids.polymorphism_data_opts, [SequenceSimilarity(),
SegregatingSites(),
LVCorrelation(),
AFS()]],
"Outlier detection":
[self.screen.ids.outlier_opts, [OutlierMissing(),
OutlierSegregating(),
OutlierSize()]]}
# Get active type
main_gl = self.screen.ids.main_stats_opts
for gl in [x for x in main_gl.children if isinstance(x, OptsGrid)]:
if gl.active_grid:
active_gl = gl
if active_gl.grid_name == idx:
bt.disabled = False
return
else:
active_gl.clear_widgets()
Animation(height=5, d=.32, t="in_quad").start(active_gl)
active_gl.active_grid = False
# Update sink grid height
gl_height = sum(x.height + 10 for x in wgts[idx][1]) + 5
Animation(height=gl_height, d=.32, t="in_quad").start(wgts[idx][0])
Clock.schedule_once(
lambda x: transfer_wgts(wgts[idx][1], wgts[idx][0]), .32)
[docs] def stats_write_plot(self, plot_data, footer, plt_idx):
"""
Provided with the data structure and a plt_idx string identifier,
this function will create the plot file and app variable,
and load it into the Statistics screen.
To configure the Gene/Species/Average plot trigger, this method
impors the stats_compliant object from data/resources/stats.py
:param plot_data: list/np array, data structure to be used in plot
construction
:param footer: List, containing information on the number of
active genes and taxa
:param plt_idx: string, identification string of the plot. Usually
is the text property of the issuing button.
"""
if isinstance(plot_data, EmptyAlignment):
return self.dialog_floatcheck(
"Active alignment is empty", t="error")
if plot_data:
if "exception" in plot_data:
if plot_data["exception"] == "empty_data":
self.screen.ids.plot_content.clear_widgets()
self.dialog_floatcheck(
"No data available for plotting", t="error")
elif plot_data["exception"] == "single_alignment":
self.screen.ids.plot_content.clear_widgets()
self.dialog_floatcheck(
"Selected plot cannot be executed on single "
"alignments", t="error")
self.screen.ids.footer_box.clear_widgets()
self.populate_stats_footer(["NA", "NA"])
return self.dismiss_plot_wgt()
# Set new plot attributes
self.current_plot, self.current_lgd, self.current_table = \
self.stats_plt_method[plt_idx][0](**plot_data)
# Save plot elements in a backup. This backup can then be
# accessed when fast switching plots.
self.plot_backups[plt_idx] = self.current_table
pickle.dump(self.current_plot,
open(join(self.temp_dir, plt_idx.replace("/", "")),
"wb"))
if self.current_lgd:
self.current_plot.savefig(
join(self.temp_dir, self.stats_plt_method[plt_idx][1]),
bbox_extra_artists=(self.current_lgd,),
bbox_inches="tight", dpi=200)
else:
self.current_plot.savefig(
join(self.temp_dir, self.stats_plt_method[plt_idx][1]),
bbox_inches="tight", dpi=200)
# Dismiss stats toggle widget, if present
self.dismiss_stats_toggle()
if plt_idx in stats_compliant:
# Show toggle widget
self.show_stats_toggle(**stats_compliant[plt_idx])
else:
self.previous_stats_toggle = None
# List of plots for which an horizontal separator is available:
hseparator_plots = ["Pairwise sequence similarity gn"]
# List of plots with a switcher between absolute values and
# proportions
prop_plots = [["Segregating sites",
"Allele Frequency Spectrum"],
["Segregating sites prop",
"Allele Frequency Spectrum prop"]]
# List of plots with outlier footer
outlier_plots = ["Missing data outliers",
"Missing data outliers sp",
"Segregating sites outliers",
"Segregating sites outliers sp",
"Sequence size outliers",
"Sequence size outliers sp"]
# Adds or removes the horizontal threshold option slider from the
# Screen footer
if plt_idx in hseparator_plots:
self.screen.ids.footer_box.clear_widgets()
hwgt = HseparatorFooter()
ylims = self.current_plot.gca().get_ylim()
hwgt.ids.slider.min, hwgt.ids.slider.max = \
[int(x) for x in ylims]
hwgt.plt_file = self.stats_plt_method[plt_idx][1]
self.screen.ids.footer_box.add_widget(hwgt)
# Adds or removes the Absolute/Proportion switcher from the footer
elif plt_idx in prop_plots[0] + prop_plots[1]:
swt = PropSwitcher()
try:
prop_idx = prop_plots[1][prop_plots[0].index(plt_idx)]
abs_idx = plt_idx
swt.ids.abs.state = "down"
except ValueError:
abs_idx = prop_plots[0][prop_plots[1].index(plt_idx)]
prop_idx = plt_idx
swt.ids.proportion.state = "down"
# Clear footer box
self.screen.ids.footer_box.clear_widgets()
# Create PropSwitcher instance and provide plt_idx to checkboxes
swt.ids.abs.plt_idx = abs_idx
swt.ids.proportion.plt_idx = prop_idx
self.screen.ids.footer_box.add_widget(swt)
# Adds or removes outlier footer
elif plt_idx in outlier_plots:
self.screen.ids.footer_box.clear_widgets()
outlier_wgt = OutlierFooter()
# Outliers are present when self.current_table is not None
if self.current_table:
outlier_num = len(self.current_table)
# Set label with number of outliers
outlier_wgt.ids.outlier_number.text = \
"{} outliers found".format(outlier_num)
# Set the data set type based on the ending of the
# plt_idx. Species plots always end with " sp"
if plt_idx.endswith(" sp"):
outlier_wgt.ds_type = "Taxa"
else:
outlier_wgt.ds_type = "Files"
else:
# If no outliers were found, disabled all buttons
outlier_wgt.ids.o_rm.disabled = True
outlier_wgt.ids.o_exp.disabled = True
outlier_wgt.ids.o_view.disabled = True
outlier_wgt.ids.outlier_number.text = "No outliers found"
self.screen.ids.footer_box.add_widget(outlier_wgt)
else:
self.screen.ids.footer_box.clear_widgets()
# Add plot toolbar
self.show_plot_toolbar(toolbar_type="stats")
self.load_plot(
join(self.temp_dir, self.stats_plt_method[plt_idx][1]),
self.screen.ids.plot_content)
if footer:
self.populate_stats_footer(footer)
[docs] def stats_sethline(self, val, plt_file, inverted=False):
"""
Sets an horizontal threshold bar to the current plot object
:param val: integer, The y-axis value for the horizontal line
:param plt_file: string, The path to the plot file
:param inverted: Boolean, Determine whether the shade will be
above or below the plot
"""
# Get plot limits for patch position and size
xlim = self.current_plot.gca().get_xlim()
ylim = self.current_plot.gca().get_ylim()
# Removes previous patch, if present
try:
self.plt_patch.remove()
except AttributeError:
pass
# Get position and size
if inverted:
xy = (xlim[0], val)
height = ylim[1] - val
else:
xy = (xlim[0], ylim[0])
height = val - ylim[0]
self.plt_patch = patches.Rectangle(
xy, xlim[1], height, edgecolor="grey", fc="#dcdcdc",
alpha=.7, zorder=10)
self.current_plot.gca().add_patch(self.plt_patch)
self.current_plot.savefig(join(self.temp_dir, plt_file),
bbox_inches="tight", dpi=200)
self.load_plot(join(self.temp_dir, plt_file),
self.screen.ids.plot_content)
[docs] def stats_select_plot(self, gn_idx, sp_idx, avg_idx):
"""
Dialog used for plot type selection when choosing an option from the
stats panel
"""
# Check whether there is data loaded
if not self.file_list:
return self.dialog_floatcheck("Warning: No data has been "
"loaded into the app", t="error")
# Check if active file list is empty
if not self.active_file_list:
return self.dialog_floatcheck("Warning: There are no active "
"files selected", t="error")
wgts = ["gene", "sp", "avg"]
content = PlotTriageDialog(cancel=self.dismiss_popup)
for w, f in zip(wgts, [gn_idx, sp_idx, avg_idx]):
if not f:
content.ids[w].disabled = True
else:
content.ids[w].plt_idx = f
self.show_popup(title="Select plot type", content=content,
size=(400, 180))
[docs] def stats_handle_outliers(self, operation, ds_type):
"""
Function that handles the processing of outliers in the stats plots
:param operation: string, three operations are available:
.:remove: Removes outliers from data set
.:export: Export outliers to text file
.:view: Opens a new dialog with the list of outliers
:param ds_type: string, two ds_types are available:
.:Taxa: Removes taxa
.:Files: Removes files
"""
# Remove elements from app
if operation == "remove":
# The removal of taxa/files uses the export_table function to
# dump the outliers into a file and then the
# remove_bt_from_file function to remove them form the app
self.export_table(self.temp_dir, "outlier_temp", warning_dlg=False)
self.remove_bt_from_selection(ds_type,
txt_file=join(self.temp_dir,
"outlier_temp.csv"))
# Remove temporary outlier text file
os.remove(join(self.temp_dir, "outlier_temp.csv"))
# Refresh plot after outlier removal
self.stats_show_plot(self.current_plt_idx[0],
self.current_plt_idx[1])
self.dialog_floatcheck(
"{} outliers where removed".format(len(self.current_table)),
t="info")
if operation == "export":
self.dialog_filechooser("export_outliers")
if operation == "view":
content = InputList(cancel=self.dismiss_popup)
content.ids.ok_bx.remove_widget(content.ids.ok_bt)
content.ids.ok_bx.width = 100
for outlier in self.current_table:
bt = TFButton(text=outlier[0], height=30)
content.ids.rev_inlist.add_widget(bt)
self.show_popup(
title="{} {} outlier(s)".format(len(self.current_table),
ds_type),
content=content, size=(450, 500))
[docs] def get_active_sets(self, file_set_name=None, taxa_set_name=None):
"""
Returns a tuple with the file set list as first element and taxa
set list as second element. List sets are only return for non
None arguments. If, for example, only file_set_name is provided,
then the return tuple will be ([file list], None)
:param file_set_name: string, name of file set
:param taxa_set_name: string, name of file set
"""
if file_set_name:
if file_set_name == "All files":
file_set = [x for x in self.file_list]
elif file_set_name == "Active files":
file_set = [x for x in self.active_file_list]
else:
file_set = [self.filename_map[x] for x in
self.file_groups[file_set_name]]
else:
file_set = None
if taxa_set_name:
if taxa_set_name == "Active taxa":
taxa_set = [x for x in self.active_taxa_list]
elif taxa_set_name == "All taxa":
taxa_set = [x for x in self.alignment_list.taxa_names]
else:
taxa_set = self.taxa_groups[taxa_set_name]
else:
taxa_set = None
return file_set, taxa_set
[docs] def stats_show_plot(self, plt_idx, additional_args=None):
"""
Wrapper that executes plot data gathering and execution. The method
that gathers the data for plot production runs in the background.
Once it's finished, that data is fed to the stats_write_plot
method that will create the plot file and load it into the program.
:param plt_idx: string, identification string of the plot. Usually
is the text property of the issuing button.
:param additional_args:
"""
# Interrupt summary statistics, if running
self.terminate_stats = True
# Reset scatter properties for plots
self.screen.ids.plot_content.do_scale = True
self.screen.ids.plot_content.do_translation = True
self.screen.ids.plot_content.do_rotation = True
# Check whether there is data loaded
if not self.file_list:
return self.dialog_floatcheck("Warning: No data has been "
"loaded into the app", t="error")
# Check if active file list is empty
if not self.active_file_list:
return self.dialog_floatcheck("Warning: There are no active "
"files selected", t="error")
# Set active file and taxa sets
file_set_name = self.screen.ids.active_file_set.text
taxa_set_name = self.screen.ids.active_taxa_set.text
# Note: When setting file_set, the objects file_list,
# active_taxa_list, etc should not be directly assigned since
# changes to these objects will also change the taxa_set,
# and consequently, the self.previous_sets
file_set, taxa_set = self.get_active_sets(file_set_name,
taxa_set_name)
# List of gene specific plots. These are always removed
gene_specific = {"Pairwise sequence similarity gn":
"similarity_distribution_gn.png",
"Segregating sites gn": "segregating_sites_gn.png",
"Allele Frequency Spectrum gn":
"allele_frequency_spectrum_gn.png"}
# Remove gene specific plots if they exist
if plt_idx in gene_specific:
try:
os.remove(join(self.temp_dir, gene_specific[plt_idx]))
except EnvironmentError:
pass
# This will check if the current data sets are different from the
# previous. If not so, it will then check if there is a temporary
# plot file for the current plt_idx. If so, do not run
# get_stats_data and show the previous plot instead.
if file_set == self.previous_sets["Files"] and \
taxa_set == self.previous_sets["Taxa"]:
# Checks if the figure file for the selected plot already exists
if os.path.exists(join(self.temp_dir,
self.stats_plt_method[plt_idx][1])):
self.toggle_stats_panel(force_close=True)
# Get the current plot from the backup
self.current_table = self.plot_backups[plt_idx]
self.current_plot = pickle.load(
open(join(self.temp_dir, plt_idx.replace("/", "")), "rb"))
return self.stats_write_plot(None, None, plt_idx)
else:
# Update previous sets
self.previous_sets["Files"] = file_set
self.previous_sets["Taxa"] = taxa_set
# Remove previous plots when data sets are modified
for p in self.stats_plt_method.values():
try:
os.remove(join(self.temp_dir, p[1]))
except OSError:
pass
self.run_in_background(
func=get_stats_data,
second_func=self.stats_write_plot,
args1=[self.alignment_list, plt_idx, file_set,
list(taxa_set), additional_args,
"use_ns"],
args2=[plt_idx])
self.toggle_stats_panel(force_close=True)
# Sets the current_plt_idx so that plots can be updated and
# reload the current plot
self.current_plt_idx = [plt_idx, additional_args]
[docs] def dialog_select_gene(self, plt_idx):
"""
Generates dialog for selecting gene for single gene plot creation
"""
no_window = ["Allele Frequency Spectrum gn"]
content = SelectGeneDialog(cancel=self.dismiss_popup)
# Remove sliding window parameter if plt_idx does not use it
if plt_idx in no_window:
content.remove_widget(content.ids.sliding_window)
content.plt_idx = plt_idx
# By default show up to 20 files at first
for i in range(20):
try:
f = basename(self.file_list[i])
bt = TGToggleButton(text=f, id=f, state="normal", height=30,
shorten=True, shorten_from="right",
disabled_color=(1, 1, 1, 1),
background_disabled_down=join("data", "backgrounds",
"bt_process.png"))
bt.text_size[0] = bt.size[0] * 3
bt.bind(on_release=self.toggle_groups)
content.ids.gn_grid.add_widget(bt)
content.gene_counter += 1
except IndexError:
break
try:
if self.file_list[content.gene_counter + 1]:
content.ids.gn_grid.add_widget(StatsMoreBt())
except IndexError:
pass
self.show_popup(title="Select gene for sliding window analysis...",
content=content, size_hint=(.4, .9))
[docs] def stats_search_genes(self, txt):
"""
Searches loaded genes for the single gene display of the Statistics
screen
"""
# When empty search, clear grid layout
if txt == "":
return self._popup.content.ids.gn_grid.clear_widgets()
self._popup.content.ids.gn_grid.clear_widgets()
found_bts = [basename(x) for x in self.file_list if
txt.lower() in basename(x).lower()]
for f in found_bts:
bt = TGToggleButton(text=f, id=f, state="normal", height=30,
shorten=True, shorten_from="right",
disabled_color=(1, 1, 1, 1),
background_disabled_down=join("data", "backgrounds",
"bt_process.png"))
bt.text_size[0] = bt.size[0] * 3
bt.bind(on_release=self.toggle_groups)
self._popup.content.ids.gn_grid.add_widget(bt)
[docs] def stats_load_more_genes(self):
"""
Functionality to the load next 20 genes in the stats gene selection
dialog
"""
# Remove previous StatsMorebt
self._popup.content.ids.gn_grid.remove_widget(
self._popup.content.ids.gn_grid.children[0])
for i in range(self._popup.content.gene_counter,
self._popup.content.gene_counter + 20):
try:
f = basename(self.file_list[i])
bt = TGToggleButton(text=f, id=f, state="normal", height=30,
shorten=True, shorten_from="right",
disabled_color=(1, 1, 1, 1),
background_disabled_down=join("data", "backgrounds",
"bt_process.png"))
bt.text_size[0] = bt.size[0] * 3
bt.bind(on_release=self.toggle_groups)
self._popup.content.ids.gn_grid.add_widget(bt)
except IndexError:
break
try:
if self.file_list[self._popup.content.gene_counter + 1]:
self._popup.content.ids.gn_grid.add_widget(StatsMoreBt())
except IndexError:
pass
[docs] def stats_clear_search(self):
"""
Clears stats gene search to default buttons
"""
self._popup.content.ids.gn_grid.clear_widgets()
for i in range(20):
try:
f = basename(self.file_list[i])
bt = TGToggleButton(text=f, id=f, state="normal", height=30,
shorten=True, shorten_from="right",
disabled_color=(1, 1, 1, 1),
background_disabled_down=join("data", "backgrounds",
"bt_process.png"))
bt.text_size[0] = bt.size[0] * 3
bt.bind(on_release=self.toggle_groups)
self._popup.content.ids.gn_grid.add_widget(bt)
except IndexError:
break
# ##################################
#
# CORE RELATED METHODS AND FUNCTIONS
#
# ##################################
[docs] def load_files_subproc(self, files):
multiprocessing.freeze_support()
def check_proc(p, dt):
try:
content.ids.pb.value = shared_ns.progress
content.ids.msg.text = shared_ns.m
except AttributeError:
pass
if not p.is_alive():
Clock.unschedule(func)
self.dismiss_popup()
try:
if shared_ns.exception == "multiple_type":
return self.dialog_warning(
"Multiple sequence types detected",
"The selected input alignments contain more "
"than one sequence type (DNA, RNA, Protein). "
"Please select input files of the same "
"sequence type")
if shared_ns.exception:
return self.dialog_floatcheck(
"Unexpected error when loading input data. "
"Check app logs", t="error")
except:
pass
# Get the alignment object from the child thread and load
# it into the app
aln_obj = queue.get()
self.load_files(file_list, aln_obj)
# Shutting down manager that provides communication between
# parent and child thread
manager.shutdown()
# Join child process and exit
p.join()
return
# Kill switch
if self.terminate_load_files:
content.ids.msg.text = "Canceling..."
# Issuing the kill order to the child thread. This signal
# will propagate through the worker's methods and graciously
# terminate
shared_ns.stop = True
# This small delay seems to fix manager shutdown issues
time.sleep(.1)
# Shutting down manager that provides communication between
# parent and child thread
manager.shutdown()
# Close loading popup dialog
self.dismiss_popup()
# Unschedule the current function
Clock.unschedule(func)
# Clean up alignments that were already loaded into the
# AlignmentList object
self.alignment_list.remove_file(file_list)
# Join child process and exit
p.join()
return
# Check if proteome files have already been loaded. If so, issue
# warning, since the two data types cannot be loaded in simultaneous
if self.proteome_files:
return self.dialog_warning("Cannot mix alignment and proteome "
"data",
"TriFusion currently has proteome data"
" loaded. If you want to load alignment"
" data, please remove the current"
" data set.")
# To support for opening all files in one or more directories, all
# entries in files will be checked if they are directories. If so,
# all files in that directory will be appended to file_list instead
file_list = []
for i in files:
if os.path.isdir(i):
file_list.extend([join(i, x) for x in
os.listdir(i)
if os.path.isfile(join(i, x))])
elif os.path.isfile(i):
file_list.append(i)
else:
# If the file has not been found, try joining the called_dir
# path with the file name
mod_path = join(called_dir, os.path.split(i)[1])
if os.path.isfile(mod_path):
file_list.append(mod_path)
file_list = sorted(file_list)
if not file_list:
return self.dialog_floatcheck("No valid input files were"
" provided", t="error")
# Set up manager and shared name space to share information
# between background and main processes
manager = multiprocessing.Manager()
shared_ns = manager.Namespace()
queue = Queue.Queue()
# Initialize stop flag to allow thread to be killed by the user
shared_ns.stop = False
# Remove lock from background process
self.terminate_load_files = False
# Create process
p = threading.Thread(
target=load_proc,
args=(self.alignment_list, file_list, shared_ns,
queue))
p.daemon = True
p.start()
# Remove any possible previous popups
self.dismiss_popup()
# Create loading process dialog
content = LoadProgressDialog()
content.ids.msg.text = "Initializing"
content.ids.pb.max = len(file_list)
# Give functionality to cancel button
content.ids.close_bt.bind(
on_release=lambda j: setattr(self, "terminate_load_files",
True))
self.show_popup(title="Loading files", content=content,
size=(400, 300), auto_dissmiss=False)
# Schedule function that checks the process' pulse
func = partial(check_proc, p)
Clock.schedule_interval(func, .01)
[docs] def load_files(self, selection=None, aln_list=None):
"""
Loads the selected input files into the program using the
AlignmentList object provided by aln_list. The loading process is
divided in phases:
.: Load the AlignmentList object from a pickle object
.: Check for invalid alignments (duplicates, invalid input
formats, sequences with unequal length) and issue warning message
.: If any valid input files passed all checks, then the app
structures are updated
To allow different files to be loaded in different occasions,
all checks are performed on the aln_list object, and not in the
app attribute self.alignment_list
:param aln_pickle: string, path to pickle file with the
AlignmentObject
:param selection: list, with the path of all files provided to
the app
"""
# Check for consistency in sequence type across alignments
if self.sequence_types:
current_seq_type = set([self.sequence_types] +
aln_list.format_list())
else:
current_seq_type = aln_list.format_list()
if not self.sequence_types and current_seq_type:
self.sequence_types = list(current_seq_type)[0]
# Set the new alignment_list attribute
self.alignment_list = aln_list
# Updating active alignment list
self.active_taxa_list = self.alignment_list.taxa_names
# removes bad alignment files from selection list
selection = [path for path in selection if path not in
aln_list.bad_alignments + aln_list.non_alignments]
# If duplicate alignments were loaded, issue a warning
if self.alignment_list.duplicate_alignments:
self.dialog_floatcheck(
"Duplicate input alignments detected and ignored",
t="error")
# Reset the duplicate alignment storage, so that it doesn't
# issue the warning every time data is loaded
self.alignment_list.duplicate_alignments = []
# Checking if there are invalid input alignments
if aln_list.bad_alignments or aln_list.non_alignments:
msg = ""
if aln_list.bad_alignments:
msg += "The following input file(s) could not be open:"\
"\n\n[b]%s[/b]\n\n" % "\n".join(basename(x) for x in
aln_list.bad_alignments)
# Reset the bad_alignments attribute to avoid triggering
# this in subsequent loading operations
self.alignment_list.bad_alignments = []
if aln_list.non_alignments:
msg += "The following input file(s) contain(s) " \
"sequences of unequal length:\n\n[b]%s[/b]" % \
"\n".join(basename(x) for x in aln_list.non_alignments)
# Reset the non_alignments attribute to avoid triggering
# this in subsequent loading operations
self.alignment_list.non_alignments = []
# After gathering all information on bad input files and
# non-aligned sequences, issue the error message
self.dialog_warning("Invalid input file(s) detected", msg)
# If new alignments have been provided update app structures. If
# all provided alignments were bad, ignore this
if selection:
# If data has been previously loaded, updated these attributes
if self.file_list:
# Updating complete and active file lists
self.file_list.extend(selection)
self.active_file_list.extend(selection)
# Update the filename - path mapping attribute
self.filename_map = dict(
list(self.filename_map.items()) + list((x, y) for x, y in
zip([basename(x) for x in selection], selection)))
# If no data has been previously loaded, set the attributed
else:
# Set an attribute with the input file list
self.file_list = selection
# Setting active file list and path list
self.active_file_list = deepcopy(self.file_list)
# Sett the filename - path mapping attribute
self.filename_map = dict((x, y) for x, y in zip(
[basename(x) for x in selection], selection))
# Update active taxa list
self.update_taxa()
# Populates files and taxa contents
self.update_tabs()
# Gathers taxa and file information
self.get_taxa_information()
# Issue final dialog with the number of files successfully
# loaded
self.dialog_floatcheck(
"%s file(s) successfully loaded" % len(selection),
t="info")
# If on statistics screen, issue the summary statistics overview
if self.current_screen == "Statistics":
self.statistics_show_summary()
@staticmethod
[docs] def update_process_switch(self, switch_id, state):
"""
Listens and updates the attribute process_switches when their state
changes.
:param switch_id: string, name of the switch according to the keys
in process_switches
:param state: Boolean, current state of the corresponding switch
"""
if switch_id in self.secondary_operations:
self.secondary_operations[switch_id] = state
else:
self.secondary_options[switch_id] = state
[docs] def dialog_orto_execution(self):
"""
Creates and populates the pre-execution dialog for orthology search
"""
# Check for input proteomes
if not self.proteome_files:
return self.dialog_floatcheck(
"Please provide proteome files as input data", t="error")
# Check for output directory
if self.ortho_dir == "":
return self.dialog_floatcheck(
"Please specify an output directory for orthology results",
t="error")
# Check for the USEARCH executable
if not self.usearch_file:
return self.dialog_usearch_fix()
# check for the MCL executable
if not self.mcl_file:
return self.dialog_mcl_fix()
content = OrtoExecutionDialog(cancel=self.dismiss_popup)
self.show_popup(
title="Orthology search execution summary - Processing %s"
" file(s)" % len(self.active_proteome_files),
content=content,
size=(550, 350))
# Correct minimum species filter
if self.orto_min_sp > len(self.active_proteome_files):
self.orto_min_sp = len(self.active_proteome_files)
self.dialog_floatcheck(
"Corrected the minimum number of taxa per cluster value "
"to the maximum number allowed by the active proteomes.",
t="warning")
content.ids.gene_filter.text = \
"[b][size=18][color=37abc8ff]Maximum number of gene copies " \
"per cluster:[/color][/size][/b] %s" %\
self.orto_max_gene
content.ids.sp_filter.text = \
"[b][size=18][color=37abc8ff]Minimum number of taxa per " \
"cluster:[/color][/size][/b] %s" %\
self.orto_min_sp
content.ids.eval.text =\
"[b][size=18][color=37abc8ff]USEARCH e-value threshold:" \
"[/color][/size][/b] %s" %\
self.usearch_evalue
content.ids.inflation.text = \
"[b][size=18][color=37abc8ff]MCL inflation value(s):" \
"[/color][/size][/b] %s" %\
", ".join(self.mcl_inflation)
content.ids.threads.text = \
"[b][size=18][color=37abc8ff]Threads :[/color][/size][/b] %s" %\
self.screen.ids.usearch_threads.text
[docs] def orthology_search_exec(self):
"""
Main function that executes all queued procedures of the orthology
module
"""
tasks_map = {"schema": "Database setup",
"adjust": "Adjusting Fasta files",
"filter": "Filtering Fasta files",
"usearch": "Running USearch",
"parse": "Parsing USearch output",
"pairs": "Obtaining pairs",
"mcl": "Running MCL",
"dump": "Dumping groups",
"filter_groups": "Filtering group files"}
wgt_ref = {}
def finish_op(wgt, op_name):
wgt.ids.load_box.clear_widgets()
check_wgt = ProgressFinished()
wgt.ids.load_box.add_widget(check_wgt)
wgt.ids.main_lbl.font_size = 18
wgt.ids.main_lbl.color = (.7, .7, .7, 1)
wgt.ids.main_lbl.text = tasks_map[op_name]
wgt.ids.secondary_lbl.text = ""
wgt.ids.secondary_lbl.color = (.7, .7, .7, 1)
closed_ops.append(op_name)
def start_op(wgt):
wgt.ids.load_box.clear_widgets()
spinner = LoadSpinner()
counter = LoadCounter()
wgt_ref["spinner"] = spinner
wgt_ref["counter"] = counter
wgt.ids.load_box.add_widget(counter)
wgt.ids.load_box.add_widget(spinner)
wgt.ids.main_lbl.font_size = 18
wgt.ids.main_lbl.color = self._blue
wgt.ids.secondary_lbl.color = self._blue
def check_process(p, dt):
"""
Checks the status of the background process "p" and updates
the progress dialog label
"""
# Listens for cancel signal
if self.terminate_orto_search:
# Issuing the kill order to the child thread. This signal
# will propagate through the worker's methods and graciously
# terminate
shared_ns.stop = True
# If a subprocess has been issued, kill it
try:
if shared_ns.subp:
os.kill(shared_ns.subp, signal.SIGINT)
shared_ns.subp = None
except AttributeError:
pass
# This small delay seems to fix manager shutdown issues
time.sleep(.1)
# Shutting down manager that provides communication between
# parent and child thread
manager.shutdown()
# Close loading popup dialog
self.dismiss_popup()
# Unschedule the current function
Clock.unschedule(func)
# Clear sqlitedb
os.remove(join(self.temp_dir, "orthoDB.db"))
# Join child process and exit
p.join()
return
# Update progress dialog
try:
if shared_ns:
# If current_op is not an empty list, it means that this
# is not the first task.
if current_op:
# Check if the task defined in the previous cycle is
# the same as the current task. If not, do this
if shared_ns.task != current_op[0]:
# Check if all finished tasks where closed
for op in [x for x in shared_ns.finished_tasks
if x not in closed_ops]:
# This will get previously finished tasks
# that were completed sooner
if op != current_op[0]:
pwgt = content.ids[op]
finish_op(pwgt, op)
# Get the previous task progress widget
pwgt = content.ids[current_op[0]]
# Finish animation for previous task
finish_op(pwgt, current_op[0])
# Set current task progress as the current_op
current_op[0] = shared_ns.task
# Get the current task progress widget
cwgt = content.ids[shared_ns.task]
# Start progress animations for current task
start_op(cwgt)
else:
pass
# This means that this is the first task
else:
# Add the first task to current_op
current_op.append(shared_ns.task)
# Get current progress task widget
cwgt = content.ids[shared_ns.task]
# Start progress animations for current task
start_op(cwgt)
# Update the current spinner... spin
wgt_ref["spinner"].ids.img.rotation -= 10
# Update the counter percentage
if shared_ns.total and shared_ns.counter and \
current_op[0] != "usearch" and \
current_op[0] != "mcl":
# Get percentage
perc = int((float(shared_ns.counter) /
float(shared_ns.total)) * 100.)
# Set percentage on progress dialog
wgt_ref["counter"].text = "{}%".format(perc)
# Set secondary progress label, if any
if shared_ns.msg:
content.ids[shared_ns.task].\
ids.secondary_lbl.text = shared_ns.msg
if current_op[0] == "usearch" or current_op[0] == "mcl":
# Provide uncertain counter for usearch and mcl
# operations.
wgt_ref["counter"].text = "??"
except AttributeError:
pass
if not p.is_alive():
Clock.unschedule(func)
self.dismiss_popup()
try:
if shared_ns.exception:
self.dialog_floatcheck(shared_ns.exception,
t="error")
except:
pass
try:
# Set the protein database file
self.protein_db = join(self.ortho_dir,
"backstage_files",
self.usearch_db)
self.dialog_search_report(shared_ns.stats,
shared_ns.groups)
except:
if not shared_ns.exception:
self.dialog_floatcheck("Unexpected error when "
"search orthologs. Check "
"app logs.", t="error")
manager.shutdown()
p.join()
# Create directory that will store intermediate files during
# orthology search
int_dir = join(self.ortho_dir, "backstage_files")
if not os.path.exists(int_dir):
os.makedirs(int_dir)
# Set up manager and shared name space to share information
# between background and main processes
manager = multiprocessing.Manager()
shared_ns = manager.Namespace()
shared_ns.stop = False
shared_ns.exception = None
shared_ns.total = 0
shared_ns.counter = 0
shared_ns.msg = None
current_op = []
closed_ops = []
# Remove lock from background process
self.terminate_orto_search = False
# Create Process instance
p = threading.Thread(
target=orto_execution,
args=(
shared_ns,
self.temp_dir,
self.active_proteome_files,
self.protein_min_len,
self.protein_max_stop,
self.usearch_file,
self.usearch_evalue,
self.screen.ids.usearch_threads.text,
self.usearch_output,
self.mcl_file,
self.mcl_inflation,
self.ortholog_prefix,
self.group_prefix,
self.orto_max_gene,
self.orto_min_sp,
self.sqldb,
self.ortho_dir,
self.usearch_db))
p.daemon = True
p.start()
# Remove any possible previous popups
self.dismiss_popup()
# Create Progression dialog
content = OrtoProgressDialog()
# Populate progress dialog content
for op, txt in tasks_map.items():
content.ids[op].ids.main_lbl.text = txt
self.show_popup(title="Running Orthology Search", content=content,
size=(340, 600), auto_dissmiss=False)
# Schedule function that checks the process' pulse
func = partial(check_process, p)
Clock.schedule_interval(func, .2)
[docs] def dialog_fileoverwrite(self, msg):
"""
General dialog for querying whether an existing file should be
overwritten or skipped
"""
content = FileOverwriteDialog(cancel=self.dismiss_subpopup)
content.ids.dlg_text.text = msg
self.show_popup(title="File already exists...", content=content,
size=(450, 220), popup_level=2)
[docs] def process_exec(self):
"""
Main function that executes all queued procedures of the process
module
"""
ops_map = {"reverse_concatenation": "Reverse concatenation",
"collapse_filter": "Filter (Collapse)",
"filter": "Filter",
"concatenation": "Concatenation",
"collapse": "Collapse",
"gcoder": "Gap coding",
"consensus": "Consensus",
"write": "Writing output"}
def build_progress_dialog(wgt):
# Get the main and additional file operations
main_ops = []
additional_ops = []
# Iterate over a fixed order list of possible ops
for op in ["reverse_concatenation",
"collapse_filter",
"filter",
"concatenation",
"collapse",
"gcoder",
"consensus"]:
try:
# Add to main ops if op is a main operation and set to True
if self.main_operations[op]:
main_ops.append(op)
except KeyError:
pass
try:
if self.secondary_operations[op]:
try:
# Add to additional ops if op is True for secondary
# operations and true for *_file
if self.secondary_options["{}_file".format(op)]:
additional_ops.append(op)
else:
main_ops.append(op)
except KeyError:
# Add to main ops if op is True for secondary
# operations but not True for *_file
main_ops.append(op)
except KeyError:
pass
# Set width of dialog based on the presence or not of additional
# files
if additional_ops:
width = 650
else:
width = 350
wgt.ids.additional_outputs.clear_widgets()
wgt.ids.additional_outputs.size_hint_x = None
wgt.ids.additional_outputs.width = 0
wgt.remove_widget(wgt.ids.additional_outputs)
# Set height of dialog based on the number of main/additional ops
ml = len(main_ops)
al = len(additional_ops)
max_ops = ml if ml > al else al
height = 200 + (max_ops * 50)
wgt_ref = {}
# Add main ops
for op in main_ops:
p = ProgressBox()
p.ids.main_lbl.text = ops_map[op]
wgt.ids.main_box.add_widget(p)
wgt_ref[op] = p
# Create write files operation
p = ProgressBox()
p.ids.main_lbl.text = ops_map["write"]
wgt.ids.main_box.add_widget(p)
wgt_ref["write"] = p
# Add additional ops
for op in additional_ops:
p = ProgressBox()
p.ids.main_lbl.text = ops_map[op]
wgt.ids.ad_box.add_widget(p)
wgt_ref[op] = p
# Give functionality to cancel button
content.ids.cancel_bt.bind(
on_release=lambda x: setattr(self,
"terminate_process_exec",
True))
self.show_popup(title="Process execution...", content=content,
size=(width, height), auto_dissmiss=False)
return wgt_ref
def finish_op(wgt, op_name):
wgt.ids.load_box.clear_widgets()
check_wgt = ProgressFinished()
wgt.ids.load_box.add_widget(check_wgt)
wgt.ids.main_lbl.font_size = 18
wgt.ids.main_lbl.color = (.7, .7, .7, 1)
wgt.ids.main_lbl.text = ops_map[op_name]
wgt.ids.secondary_lbl.text = ""
wgt.ids.secondary_lbl.color = (.7, .7, .7, 1)
def start_op(wgt):
wgt.ids.load_box.clear_widgets()
spinner = LoadSpinner()
counter = LoadCounter()
wgt_ref["spinner"] = spinner
wgt_ref["counter"] = counter
wgt.ids.load_box.add_widget(counter)
wgt.ids.load_box.add_widget(spinner)
wgt.ids.main_lbl.font_size = 18
wgt.ids.main_lbl.color = self._blue
wgt.ids.secondary_lbl.color = self._blue
def check_process(p, man, dt):
# Update progress dialog
try:
# Start only when the first task has been initiated
if shared_ns.task:
# If current_op is not empty, it means some task has
# already been previously provided.
if current_op:
# Check if the previously provided task does not match
# with the current task. If True, modify the progress
# dialog so that the previous task is finished and
# start the current task
if shared_ns.task != current_op[0]:
# Get widget of previous task
pwgt = wgt_ref[current_op[0]]
# Modify progress dialog of previous task so that
# it appears as finished
finish_op(pwgt, current_op[0])
# Set the previous task as the current task for
# future comparisons
current_op[0] = shared_ns.task
# Get widget of current task
cwgt = wgt_ref[shared_ns.task]
# Start loading progress of current task
start_op(cwgt)
else:
cwgt = wgt_ref[shared_ns.task]
# In this case, this is the first task being performed.
# We only need to start the loading process of the current
# task
else:
# Set the previous task as the current task
current_op.append(shared_ns.task)
# Get widget of current task
cwgt = wgt_ref[shared_ns.task]
# Start loading progress of current task
start_op(cwgt)
wgt_ref["spinner"].ids.img.rotation -= 10
if shared_ns.total and shared_ns.counter:
# Get percentage
perc = int((float(shared_ns.counter) /
float(shared_ns.total)) * 100.)
# Set percentage on progress dialog
wgt_ref["counter"].text = "{}%".format(perc)
# Set main label, if any
if shared_ns.main_msg:
cwgt.ids.main_lbl.text = shared_ns.main_msg
# Set secondary label, if any
if shared_ns.msg:
try:
cwgt.ids.secondary_lbl.text = shared_ns.msg
except ValueError:
pass
except AttributeError:
pass
# Interrupt subporcess on user demand
if self.terminate_process_exec:
shared_ns.stop = True
man.shutdown()
Clock.unschedule(check_func)
self.dismiss_all_popups()
# Removes all temporary database tables
self.alignment_list.remove_tables(
self.alignment_list.get_tables())
return
try:
content.ids.msg.text = shared_ns.msg
except AttributeError:
pass
# Checks for signal that the current file being processed
# already exists
try:
if shared_ns.file_dialog:
self.dialog_fileoverwrite("File %s already exists" %
basename(shared_ns.file_dialog))
# This will prevent the dialog from being
# re-generated while the user chooses the option
shared_ns.file_dialog = None
except AttributeError:
pass
# Transmit to the subprocess the option of overwrite/skip
try:
if self.file_overwrite:
shared_ns.status = self.file_overwrite
# If the user does not check the apply to all,
# reset the file_overwrite attribute for the next file
if not self.file_apply_all:
self.file_overwrite = None
shared_ns.status = None
else:
shared_ns.apply_all = True
except AttributeError:
pass
if not p.is_alive():
# Resets file_apply_all and file_overwrite attributes
self.file_apply_all = False
self.file_overwrite = None
Clock.unschedule(check_func)
self.dismiss_all_popups()
# Removes all temporary database tables
self.alignment_list.remove_tables(
self.alignment_list.get_tables())
p.join()
# If process execution ended with an error, issue warning.
try:
self.dialog_warning(shared_ns.exception["exception"][0],
shared_ns.exception["exception"][1])
man.shutdown()
except:
if shared_ns.proc_files == 1:
self.dialog_floatcheck(
"All Done! %s file was successfully processed"
% shared_ns.proc_files, t="info")
else:
self.dialog_floatcheck(
"All Done! %s files were successfully processed"
% shared_ns.proc_files, t="info")
if shared_ns.filtered_alns:
self.dialog_filter_report(shared_ns.filtered_alns,
shared_ns.proc_files)
man.shutdown()
# Set up manager and shared name space to share information
# between background and main processes
manager = multiprocessing.Manager()
shared_ns = manager.Namespace()
shared_ns.status = None
shared_ns.apply_all = False
shared_ns.stop = False
shared_ns.task = shared_ns.total = shared_ns.counter = \
shared_ns.msg = shared_ns.main_msg = None
shared_ns.finished_tasks = []
current_op = []
# Packing arguments to background process
process_kwargs = {"aln_list": self.alignment_list,
"file_set_name": self.process_grid_wgt.ids.active_file_set.text,
"file_list": list(self.file_list),
"filename_map": dict(self.filename_map),
"file_groups": dict(self.file_groups),
"taxa_set_name": self.process_grid_wgt.ids.active_taxa_set.text,
"active_taxa_list": list(self.active_taxa_list),
"ns": shared_ns,
"taxa_groups": dict(self.taxa_groups),
"hap_prefix": str(self.hap_prefix),
"secondary_operations": self.secondary_operations,
"secondary_options": dict(self.secondary_options),
"missing_filter_settings": list(self.missing_filter_settings),
"taxa_filter_settings": list(self.taxa_filter_settings),
"codon_filter_settings": list(self.codon_filter_settings),
"variation_filter_settings": list(self.variation_filter),
"output_file": self.output_file,
"conversion_suffix": self.conversion_suffix,
"rev_infile": self.rev_infile,
"main_operations": dict(self.main_operations),
"zorro_suffix": str(self.zorro_suffix),
"partitions_file": self.partitions_file,
"output_formats": list(self.output_formats),
"create_partfile": bool(self.create_partfile),
"use_nexus_partitions": bool(self.use_nexus_partitions),
"use_nexus_models": bool(self.use_nexus_models),
"phylip_truncate_name": bool(self.phylip_truncate_name),
"output_dir": self.output_dir,
"use_app_partitions": bool(self.use_app_partitions),
"consensus_type": self.process_options.ids.consensus_mode.text,
"ld_hat": bool(self.ld_hat),
"ima2_params": list(self.ima2_options)}
# Remove lock from background process
self.terminate_process_exec = False
# Create process
p = threading.Thread(target=process_execution,
kwargs=process_kwargs)
p.start()
# Remove any possible previous popups
self.dismiss_popup()
# Create waiting dialog
content = ProcessExecutionProgress()
wgt_ref = build_progress_dialog(content)
# Schedule function that checks the process' pulse
check_func = partial(check_process, p, manager)
Clock.schedule_interval(check_func, .1)
[docs]def main():
multiprocessing.freeze_support()
global MCL_FILE
# This will handle the pointer to the kv file whether TriFusion is a
# one-file executable, a one-dir executable, or running from source.
if getattr(sys, "frozen", False):
# Get relative path of mcl binary
mcl_path = None
if sys.platform == "linux2":
mcl_path = os.path.join("data", "resources", "mcl", "linux", "mcl")
elif sys.platform in ["win32", "cygwin"]:
if platform.architecture()[0] == "64bit":
mcl_path = os.path.join("data", "resources", "mcl", "windows",
"64bit", "mcl64.exe")
else:
mcl_path = os.path.join("data", "resources", "mcl", "windows",
"32bit", "mcl32.exe")
elif sys.platform == "darwin":
mcl_path = os.path.join("data", "resources", "mcl", "MacOS", "mcl")
# One-file
try:
# Get main kv file
kv_file = os.path.join(sys._MEIPASS, "trifusion.kv")
os.chdir(sys._MEIPASS)
# Get mcl executable path
MCL_FILE = os.path.join(sys._MEIPASS, mcl_path)
# One-dir
except AttributeError:
kv_file = "trifusion.kv"
MCL_FILE = mcl_path
# Source
elif __file__:
kv_file = "trifusion.kv"
TriFusionApp(kv_file="trifusion.kv").run()
if __name__ == "__main__":
main()