/* File: blxWindow.c * Author: Gemma Barson, 2009-11-24 * Copyright (c) 2009 - 2012 Genome Research Ltd * --------------------------------------------------------------------------- * SeqTools 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 3 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * or see the on-line version at http://www.gnu.org/copyleft/gpl.txt * --------------------------------------------------------------------------- * This file is part of the SeqTools sequence analysis package, * written by * Gemma Barson (Sanger Institute, UK) * * based on original code by * Erik Sonnhammer (SBC, Sweden) * * and utilizing code taken from the AceDB and ZMap packages, written by * Richard Durbin (Sanger Institute, UK) * Jean Thierry-Mieg (CRBM du CNRS, France) * Ed Griffiths (Sanger Institute, UK) * Roy Storey (Sanger Institute, UK) * Malcolm Hinsley (Sanger Institute, UK) * * Description: See blxWindow.h *---------------------------------------------------------------------------- */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DEFAULT_WINDOW_BORDER_WIDTH 1 /* used to change the default border width around the blixem window */ #define DEFAULT_COVERAGE_VIEW_BORDER 12 /* size of border to allow around the coverage view */ #define DEFAULT_FONT_SIZE_ADJUSTMENT -2 /* used to start with a smaller font than the default widget font */ #define DEFAULT_SCROLL_STEP_INCREMENT 5 /* how many bases the scrollbar scrolls by for each increment */ #define DEFAULT_WINDOW_WIDTH_FRACTION 0.9 /* what fraction of the screen size the blixem window width defaults to */ #define DEFAULT_WINDOW_HEIGHT_FRACTION 0.6 /* what fraction of the screen size the blixem window height defaults to */ #define MATCH_SET_GROUP_NAME "Match set" #define LOAD_DATA_TEXT "Load optional\ndata" #define DEFAULT_TABLE_XPAD 2 /* default x-padding to use in tables */ #define DEFAULT_TABLE_YPAD 2 /* default y-padding to use in tables */ #define MAX_RECOMMENDED_COPY_LENGTH 100000 /* warn if about to copy text longer than this to the clipboard */ typedef enum {SORT_TYPE_COL, SORT_TEXT_COL, N_SORT_COLUMNS} SortColumns; /* Utility struct used when comparing sequences to a search string */ typedef struct _CompareSeqData { const char *searchStr; /* the string to search for */ BlxColumnId searchCol; /* the column ID, which defines what data to search e.g. Name or Tissue Type */ BlxViewContext *bc; /* the main context */ GList *matchList; /* resulting list of all BlxSequences that match */ GError *error; } SeqSearchData; /* Properties specific to the blixem window */ typedef struct _BlxWindowProperties { GtkWidget *bigPicture; /* The top section of the view, showing a "big picture" overview of the alignments */ GtkWidget *detailView; /* The bottom section of the view, showing a detailed list of the alignments */ GtkWidget *mainmenu; /* The main menu */ GtkWidget *seqHeaderMenu; /* The context menu for tree headers */ GtkActionGroup *actionGroup; /* The action-group for the menus */ BlxViewContext *blxContext; /* The blixem view context */ GtkPageSetup *pageSetup; /* Page setup for printing */ GtkPrintSettings *printSettings; /* Used so that we can re-use the same print settings as a previous print */ } BlxWindowProperties; /* Local function declarations */ static BlxWindowProperties* blxWindowGetProperties(GtkWidget *widget); static void onHelpMenu(GtkAction *action, gpointer data); static void onAboutMenu(GtkAction *action, gpointer data); static void onQuit(GtkAction *action, gpointer data); static void onPrintMenu(GtkAction *action, gpointer data); static void onPageSetupMenu(GtkAction *action, gpointer data); static void onSettingsMenu(GtkAction *action, gpointer data); static void onLoadMenu(GtkAction *action, gpointer data); static void onCopySeqsMenu(GtkAction *action, gpointer data); static void onCopySeqDataMenu(GtkAction *action, gpointer data); static void onCopySeqDataMarkMenu(GtkAction *action, gpointer data); static void onCopyRefSeqDnaMenu(GtkAction *action, gpointer data); static void onCopyRefSeqDisplayMenu(GtkAction *action, gpointer data); static void onSortMenu(GtkAction *action, gpointer data); static void onZoomInMenu(GtkAction *action, gpointer data); static void onZoomOutMenu(GtkAction *action, gpointer data); static void onFindMenu(GtkAction *action, gpointer data); static void onGoToMenu(GtkAction *action, gpointer data); static void onPrevMatchMenu(GtkAction *action, gpointer data); static void onNextMatchMenu(GtkAction *action, gpointer data); static void onFirstMatchMenu(GtkAction *action, gpointer data); static void onLastMatchMenu(GtkAction *action, gpointer data); static void onPageLeftMenu(GtkAction *action, gpointer data); static void onPageRightMenu(GtkAction *action, gpointer data); static void onScrollLeft1Menu(GtkAction *action, gpointer data); static void onScrollRight1Menu(GtkAction *action, gpointer data); static void onSquashMatchesMenu(GtkAction *action, gpointer data); static void onToggleStrandMenu(GtkAction *action, gpointer data); static void onViewMenu(GtkAction *action, gpointer data); static void onCreateGroupMenu(GtkAction *action, gpointer data); static void onEditGroupsMenu(GtkAction *action, gpointer data); static void onToggleMatchSet(GtkAction *action, gpointer data); static void onDotterMenu(GtkAction *action, gpointer data); static void onCloseAllDottersMenu(GtkAction *action, gpointer data); static void onSelectFeaturesMenu(GtkAction *action, gpointer data); static void onDeselectAllRows(GtkAction *action, gpointer data); static void onStatisticsMenu(GtkAction *action, gpointer data); static gboolean onKeyPressBlxWindow(GtkWidget *window, GdkEventKey *event, gpointer data); static void onUpdateBackgroundColor(GtkWidget *blxWindow); static void onDestroyBlxWindow(GtkWidget *widget); static BlxStrand blxWindowGetInactiveStrand(GtkWidget *blxWindow); static GtkComboBox* widgetGetComboBox(GtkWidget *widget); static BlxColumnId getColumnFromComboBox(GtkComboBox *combo); static void onButtonClickedDeleteGroup(GtkWidget *button, gpointer data); static void blxWindowGroupsChanged(GtkWidget *blxWindow); static void getSequencesThatMatch(gpointer listDataItem, gpointer data); static GList* getSeqStructsFromText(GtkWidget *blxWindow, const char *inputText, const BlxColumnId searchCol, GError **error); static void createSortBox(GtkBox *parent, GtkWidget *detailView, const BlxColumnId initSortColumn, GList *columnList, const char *labelText, const gboolean searchableOnly); static GtkWidget* createCheckButton(GtkBox *box, const char *mnemonic, const gboolean isActive, GCallback callback, gpointer data); static void blxWindowSetUsePrintColors(GtkWidget *blxWindow, const gboolean usePrintColors); static gboolean blxWindowGetUsePrintColors(GtkWidget *blxWindow); static void blxWindowFindDnaString(GtkWidget *blxWindow, const char *inputSearchStr, const int startCoord, const gboolean searchLeft, const gboolean findAgain, GError **error); static GList* findSeqsFromList(GtkWidget *blxWindow, const char *inputText, const BlxColumnId inputCol, const gboolean rememberSearch, const gboolean findAgain, GError **error); static int getSearchStartCoord(GtkWidget *blxWindow, const gboolean startBeginning, const gboolean searchLeft); static GList* findSeqsFromColumn(GtkWidget *blxWindow, const char *inputText, const BlxColumnId searchCol, const gboolean rememberSearch, const gboolean findAgain, GError **error); static GtkWidget* dialogChildGetBlxWindow(GtkWidget *child); static void killAllSpawned(BlxViewContext *bc); static void calculateDepth(BlxViewContext *bc); static gdouble calculateMspData(MSP *mspList, BlxViewContext *bc); static gboolean setFlagFromButton(GtkWidget *button, gpointer data); static void copySelectedSeqDataToClipboard(GtkWidget *blxWindow); static void copySelectedSeqRangeToClipboard(GtkWidget *blxWindow, const int fromIdx, const int toIdx); static void copyRefSeqToClipboard(GtkWidget *blxWindow, const int fromIdx_in, const int toIdx_in); static void copyRefSeqTranslationToClipboard(GtkWidget *blxWindow, const int fromIdx_in, const int toIdx_in); /* MENU BUILDERS */ /* Standard menu entries */ static const GtkActionEntry mainMenuEntries[] = { { "CopyMenuAction", NULL, "Copy"}, { "Quit", GTK_STOCK_QUIT, "_Quit", "Q", "Quit Ctrl+Q", G_CALLBACK(onQuit)}, { "Help", GTK_STOCK_HELP, "_Help", "H", "Display help Ctrl+H", G_CALLBACK(onHelpMenu)}, { "About", GTK_STOCK_ABOUT, "About", NULL, "Program information", G_CALLBACK(onAboutMenu)}, { "Print", GTK_STOCK_PRINT, "_Print...", "P", "Print Ctrl+P", G_CALLBACK(onPrintMenu)}, { "PageSetup", GTK_STOCK_PAGE_SETUP, "Page set_up...", NULL, "Page setup", G_CALLBACK(onPageSetupMenu)}, { "Settings", GTK_STOCK_PREFERENCES, "_Settings...", "S", "Settings Ctrl+S", G_CALLBACK(onSettingsMenu)}, { "Load", GTK_STOCK_OPEN, "_Open features file...", NULL, "Load additional features from file Ctrl+O", G_CALLBACK(onLoadMenu)}, { "CopySeqNames", NULL, "Copy match name(s)", "C", "Copy selected match sequence's name(s) Ctrl+C", G_CALLBACK(onCopySeqsMenu)}, { "CopySeqData", NULL, "Copy match sequence (entire sequence)", NULL, "Copy whole sequence for selected match", G_CALLBACK(onCopySeqDataMenu)}, { "CopySeqDataMark", NULL, "Copy match sequence (selected section)","C","Copy selected match sequence segment Shift+Ctrl+C", G_CALLBACK(onCopySeqDataMarkMenu)}, { "CopyRefSeqDna", NULL, "Copy reference DNA", "C", "Copy selected reference sequence DNA Alt+C", G_CALLBACK(onCopyRefSeqDnaMenu)}, { "CopyRefSeqDisplay",NULL, "Copy reference translation (current frame)","C","Copy selected reference sequence translation Shift+Alt+C", G_CALLBACK(onCopyRefSeqDisplayMenu)}, { "Sort", GTK_STOCK_SORT_ASCENDING, "Sort...", NULL, "Sort sequences", G_CALLBACK(onSortMenu)}, { "ZoomIn", GTK_STOCK_ZOOM_IN, "Zoom in", "equal", "Zoom in =", G_CALLBACK(onZoomInMenu)}, { "ZoomOut", GTK_STOCK_ZOOM_OUT, "Zoom out", "minus", "Zoom out -", G_CALLBACK(onZoomOutMenu)}, { "GoTo", GTK_STOCK_JUMP_TO, "Go to position...", "P", "Go to position P", G_CALLBACK(onGoToMenu)}, { "FirstMatch", GTK_STOCK_GOTO_FIRST, "First match", "Home", "Go to first match in selection (or all, if none selected) Ctrl+Home", G_CALLBACK(onFirstMatchMenu)}, { "PrevMatch", GTK_STOCK_GO_BACK, "Previous match", "Left", "Go to previous match in selection (or all, if none selected) Ctrl+Left", G_CALLBACK(onPrevMatchMenu)}, { "NextMatch", GTK_STOCK_GO_FORWARD, "Next match", "Right", "Go to next match in selection (or all, if none selected) Ctrl+Right", G_CALLBACK(onNextMatchMenu)}, { "LastMatch", GTK_STOCK_GOTO_LAST, "Last match", "End", "Go to last match in selection (or all, if none selected) Ctrl+End", G_CALLBACK(onLastMatchMenu)}, { "BackPage", NULL, "<<", "comma", "Scroll left one page Ctrl+,", G_CALLBACK(onPageLeftMenu)}, { "BackOne", NULL, "<", "comma", "Scroll left one index ,", G_CALLBACK(onScrollLeft1Menu)}, { "FwdOne", NULL, ">", "period", "Scroll right one index .", G_CALLBACK(onScrollRight1Menu)}, { "FwdPage", NULL, ">>", "period", "Scroll right one page Ctrl+.", G_CALLBACK(onPageRightMenu)}, { "Find", GTK_STOCK_FIND, "Find...", "F", "Find sequences Ctrl+F", G_CALLBACK(onFindMenu)}, { "ToggleStrand", GTK_STOCK_REFRESH, "Toggle strand", "T", "Toggle the active strand T", G_CALLBACK(onToggleStrandMenu)}, { "View", GTK_STOCK_FULLSCREEN, "_View...", "V", "Edit view settings V", G_CALLBACK(onViewMenu)}, { "CreateGroup", NULL, "Create Group...", "G", "Create group Shift+Ctrl+G", G_CALLBACK(onCreateGroupMenu)}, { "EditGroups", GTK_STOCK_EDIT, "Edit _Groups...", "G", "Edit groups Ctrl+G", G_CALLBACK(onEditGroupsMenu)}, { "ToggleMatchSet", NULL, "Toggle _match set group", "G", "Create/clear the match set group G", G_CALLBACK(onToggleMatchSet)}, { "DeselectAllRows", NULL, "Deselect _all", "A", "Deselect all Shift+Ctrl+A", G_CALLBACK(onDeselectAllRows)}, { "Dotter", NULL, "_Dotter...", "D", "Start Dotter Ctrl+D", G_CALLBACK(onDotterMenu)}, { "CloseAllDotters", GTK_STOCK_CLOSE, "Close all Dotters", NULL, "Close all Dotters", G_CALLBACK(onCloseAllDottersMenu)}, { "SelectFeatures", GTK_STOCK_SELECT_ALL, "Feature series selection tool...", NULL, "Feature series selection tool", G_CALLBACK(onSelectFeaturesMenu)}, { "Statistics", NULL, "Statistics", NULL, "Show memory statistics", G_CALLBACK(onStatisticsMenu)} }; /* Menu entries for toggle-able actions */ static GtkToggleActionEntry toggleMenuEntries[] = { { "SquashMatches", GTK_STOCK_DND_MULTIPLE, "Squash matches", NULL, "Squash matches", G_CALLBACK(onSquashMatchesMenu), FALSE} /* must be item 0 in list */ }; /* This defines the layout of the menu for a standard user */ static const char standardMenuDescription[] = "" " " " " " " " " //" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; /* This defines the additional menu components for a developer user */ static const char developerMenuDescription[] = "" " " " " " " " " " " ""; /*********************************************************** * Utilities * ***********************************************************/ /* Return true if the current user is in our list of developers. */ static gboolean userIsDeveloper() { const gchar* developers[] = {"edgrif", "gb10"}; gboolean result = FALSE; const gchar *user = g_get_user_name(); int numDevelopers = sizeof(developers) / sizeof(gchar*); int i = 0; for (i = 0; i < numDevelopers; ++i) { if (strcmp(user, developers[i]) == 0) { result = TRUE; break; } } return result; } /* Returns the order number of the group that this sequence belongs to, or * UNSET_INT if it does not belong to a group. */ int sequenceGetGroupOrder(GtkWidget *blxWindow, const BlxSequence *seq) { SequenceGroup *group = blxWindowGetSequenceGroup(blxWindow, seq); return group ? group->order : UNSET_INT; } /* Scroll the detail view left/right by 1 base (or by 1 page, if the modifier * is pressed) */ static void scrollDetailView(GtkWidget *window, const gboolean moveLeft, const gboolean modifier) { GtkWidget *detailView = blxWindowGetDetailView(window); if (moveLeft && modifier) scrollDetailViewLeftPage(detailView); else if (moveLeft) scrollDetailViewLeft1(detailView); else if (modifier) scrollDetailViewRightPage(detailView); else scrollDetailViewRight1(detailView); } /* Move the current row selection up/down */ static gboolean moveRowSelection(GtkWidget *blxWindow, const gboolean moveUp, const gboolean ctrlModifier, const gboolean shiftModifier) { GtkWidget *detailView = blxWindowGetDetailView(blxWindow); const int activeFrame = detailViewGetActiveFrame(detailView); const BlxStrand activeStrand = detailViewGetSelectedStrand(detailView); GtkWidget *tree = detailViewGetTree(detailView, activeStrand, activeFrame); return treeMoveRowSelection(tree, moveUp, shiftModifier); } /* Move the selected base index 1 base to the left/right. Moves by individual * DNA bases (i.e. you have to move 3 bases in order to scroll a full peptide * if viewing protein matches). Scrolls the detail view if necessary to keep * the new base in view. */ static void moveSelectedBaseIdxBy1(GtkWidget *window, const gboolean moveLeft, const gboolean extend) { GtkWidget *detailView = blxWindowGetDetailView(window); DetailViewProperties *properties = detailViewGetProperties(detailView); const gboolean displayRev = detailViewGetDisplayRev(detailView); const int direction = (moveLeft == displayRev ? 1 : -1); int newDnaIdx = UNSET_INT; gboolean ok = FALSE; if (detailViewGetSelectedIdxSet(detailView)) { newDnaIdx = properties->selectedIndex->dnaIdx + direction; ok = TRUE; } if (ok) { detailViewSetSelectedDnaBaseIdx(detailView, newDnaIdx, detailViewGetActiveFrame(detailView), TRUE, TRUE, extend); } } /* Called when user pressed Home/End. If the modifier is pressed, scroll to the * start/end of all matches in the current selection (or all matches, if no * selection), or to the start/end of the entire display if the modifier is not pressed. */ static void scrollToExtremity(GtkWidget *blxWindow, const gboolean moveLeft, const gboolean modifier, const gboolean extend) { GtkWidget *detailView = blxWindowGetDetailView(blxWindow); if (modifier) { GList *selectedSeqs = blxWindowGetSelectedSeqs(blxWindow); if (moveLeft) firstMatch(detailView, selectedSeqs, extend); else lastMatch(detailView, selectedSeqs, extend); } else { const BlxSeqType seqType = blxWindowGetSeqType(blxWindow); const IntRange* const fullRange = blxWindowGetFullRange(blxWindow); if (moveLeft) setDetailViewStartIdx(detailView, fullRange->min, seqType); else setDetailViewEndIdx(detailView, fullRange->max, seqType); } } /* Jump left or right to the next/prev nearest match. Only include matches in the * current selection, if any rows are selected. */ static void goToMatch(GtkWidget *blxWindow, const gboolean moveLeft, const gboolean extend) { GList *selectedSeqs = blxWindowGetSelectedSeqs(blxWindow); if (moveLeft) { prevMatch(blxWindowGetDetailView(blxWindow), selectedSeqs, extend); } else { nextMatch(blxWindowGetDetailView(blxWindow), selectedSeqs, extend); } } /* Move the selected display index 1 value to the left/right. Moves by full peptides * if viewing protein matches. Scrolls the detail view if necessary to keep the new * index in view. If extend is true we extend the current selection range, otherwise * just move the current selection index */ static void moveSelectedDisplayIdxBy1(GtkWidget *window, const gboolean moveLeft, const gboolean extend) { DEBUG_ENTER("moveSelectedDisplayIdxBy1()"); GtkWidget *detailView = blxWindowGetDetailView(window); DetailViewProperties *detailViewProperties = detailViewGetProperties(detailView); int newSelectedBaseIdx = UNSET_INT; gboolean ok = FALSE; if (detailViewGetSelectedIdxSet(detailView)) { /* Decrement the index if moving left or increment if moving right */ newSelectedBaseIdx = detailViewProperties->selectedIndex->displayIdx; if (moveLeft) --newSelectedBaseIdx; else ++newSelectedBaseIdx; DEBUG_OUT("Moving selected display index to %d\n", newSelectedBaseIdx); ok = TRUE; } if (ok) { detailViewSetSelectedDisplayIdx(detailView, newSelectedBaseIdx, detailViewProperties->selectedIndex->frame, detailViewProperties->selectedIndex->baseNum, TRUE, TRUE, extend); detailViewRedrawAll(detailView); } DEBUG_EXIT("moveSelectedDisplayIdxBy1 returning "); } /* Zooms the display in/out. The modifiers control which section is zoomed */ static void zoomBlxWindow(GtkWidget *window, const gboolean zoomIn, const gboolean ctrl, const gboolean shift) { if (ctrl) { if (shift) { zoomWholeBigPicture(blxWindowGetBigPicture(window)); } else { zoomBigPicture(blxWindowGetBigPicture(window), zoomIn); } } else { zoomDetailView(blxWindowGetDetailView(window), zoomIn); } } /* Force a redraw of all widgets. Clears cached bitmaps etc. first */ void blxWindowRedrawAll(GtkWidget *blxWindow) { GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); bigPictureRedrawAll(bigPicture); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); detailViewRefreshAllHeaders(detailView); callFuncOnAllDetailViewTrees(detailView, widgetClearCachedDrawable, NULL); gtk_widget_queue_draw(blxWindow); } /* Utility to create a vbox with the given border and pack it into the given box. * Also put a frame around it with the given label if includeFrame is true */ static GtkWidget* createVBoxWithBorder(GtkWidget *parent, const int borderWidth, const gboolean includeFrame, const char *frameTitle) { GtkWidget *vbox = gtk_vbox_new(FALSE, 0); gtk_container_set_border_width(GTK_CONTAINER(vbox), borderWidth); if (includeFrame) { GtkWidget *frame = gtk_frame_new(frameTitle); gtk_box_pack_start(GTK_BOX(parent), frame, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(frame), vbox); } else { gtk_box_pack_start(GTK_BOX(parent), vbox, FALSE, FALSE, 0); } return vbox; } /* Utility to create an hbox with the given border and pack it into the given container */ static GtkWidget* createHBoxWithBorder(GtkWidget *parent, const int borderWidth, const gboolean includeFrame, const char *frameTitle) { GtkWidget *hbox = gtk_hbox_new(FALSE, 0); gtk_container_set_border_width(GTK_CONTAINER(hbox), borderWidth); if (includeFrame) { GtkWidget *frame = gtk_frame_new(frameTitle); gtk_container_add(GTK_CONTAINER(parent), frame); gtk_container_add(GTK_CONTAINER(frame), hbox); } else { gtk_container_add(GTK_CONTAINER(parent), hbox); } return hbox; } /* Utility to return true if any groups exist. Ignores the 'match set' group * if it doesn't have any sequences. */ static gboolean blxWindowGroupsExist(GtkWidget *blxWindow) { gboolean result = FALSE; BlxViewContext *blxContext = blxWindowGetContext(blxWindow); GList *groupList = blxContext->sequenceGroups; if (g_list_length(groupList) > 1) { result = TRUE; } else if (g_list_length(groupList) == 1) { /* Only one group. If it's the match set group, check it has sequences */ SequenceGroup *group = (SequenceGroup*)(groupList->data); if (group != blxContext->matchSetGroup || g_list_length(group->seqList) > 0) { result = TRUE; } } return result; } /* Utility to create a text entry widget displaying the given double value. The * given callback will be called when the user OK's the dialog that this widget * is a child of. */ static GtkWidget* createTextEntryString(const char *value) { GtkWidget *entry = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(entry), value); gtk_entry_set_width_chars(GTK_ENTRY(entry), strlen(value) + 2); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); return entry; } /* Utility to create a text entry widget displaying the given double value. The * given callback will be called when the user OK's the dialog that this widget * is a child of. */ static GtkWidget* createTextEntryInt(const int value) { GtkWidget *entry = gtk_entry_new(); char *displayText = convertIntToString(value); gtk_entry_set_text(GTK_ENTRY(entry), displayText); gtk_entry_set_width_chars(GTK_ENTRY(entry), strlen(displayText) + 2); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); g_free(displayText); return entry; } /* This dialog is shown when the user attempts to load a file that * is not in a natively-supported format. It asks the user what the * source should be, and allows the user to edit the coordinate range * to fetch data for. If the user enters valid values and hits OK then * the return values are populated and we return TRUE; else return FALSE. */ static gboolean showNonNativeFileDialog(GtkWidget *window, const char *filename, GString **source_out, int *start_out, int *end_out) { BlxViewContext *bc = blxWindowGetContext(window); char *title = g_strdup_printf("%sLoad Non-Native File", blxGetTitlePrefix(bc)); GtkWidget *dialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(window), (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL); g_free(title); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); GtkContainer *contentArea = GTK_CONTAINER(GTK_DIALOG(dialog)->vbox); char *labelStr = g_strdup_printf("\nFile '%s' is not a natively-supported file format.\n\nSpecify the Source to fetch data from this file using an external command\n(a fetch method for the Source must be specified in the config file)\n", filename); GtkWidget *label = gtk_label_new(labelStr); g_free(labelStr); GtkWidget *sourceEntry = createTextEntryString(""); GtkWidget *label2 = gtk_label_new("\n\nRegion to fetch data for:"); GtkWidget *startEntry = createTextEntryInt(bc->refSeqRange.min); GtkWidget *endEntry = createTextEntryInt(bc->refSeqRange.max); GtkTable *table = GTK_TABLE(gtk_table_new(5, 2, FALSE)); gtk_container_add(contentArea, GTK_WIDGET(table)); gtk_table_attach(table, label, 0, 2, 0, 1, GTK_SHRINK, GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_table_attach(table, gtk_label_new("Source"), 0, 1, 1, 2, GTK_SHRINK, GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_table_attach(table, sourceEntry, 1, 2, 1, 2, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_table_attach(table, label2, 0, 2, 2, 3, GTK_SHRINK, GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_table_attach(table, gtk_label_new("Start"), 0, 1, 3, 4, GTK_SHRINK, GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_table_attach(table, startEntry, 1, 2, 3, 4, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_table_attach(table, gtk_label_new("End"), 0, 1, 4, 5, GTK_SHRINK, GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_table_attach(table, endEntry, 1, 2, 4, 5, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); gtk_widget_show_all(dialog); gint response = gtk_dialog_run(GTK_DIALOG(dialog)); gboolean result = FALSE; if (response == GTK_RESPONSE_ACCEPT) { const gchar *source = gtk_entry_get_text(GTK_ENTRY(sourceEntry)); /* source is mandatory */ if (source && *source) { result = TRUE; *source_out = g_string_new(source); /* to do: start and end */ } } gtk_widget_destroy(dialog); return result; } /* This function loads the contents of a non-natively supported features- * file into blixem, using an external script to convert the file into * a supported file format such as GFF. A fetch method stanza must exist in the * config to define the script and its parameters. * This function asks the user what Source the file relates to so that it can * look up the fetch method that should be used. It optionally also allows the * user to specify a coordinate range to limit the fetch to.. */ static void loadNonNativeFile(const char *filename, GtkWidget *blxWindow, MSP **newMsps, GList **newSeqs, GHashTable *lookupTable, const int refSeqOffset, const IntRange* const refSeqRange, GError **error) { BlxViewContext *bc = blxWindowGetContext(blxWindow); GKeyFile *keyFile = blxGetConfig(); GString *source = NULL; int start = bc->refSeqRange.min, end = bc->refSeqRange.max; if (!showNonNativeFileDialog(blxWindow, filename, &source, &start, &end)) return; GError *tmp_error = NULL; BlxDataType *dataType = NULL; const BlxFetchMethod *fetchMethod = NULL; if (!source || !source->str) { g_set_error(&tmp_error, BLX_ERROR, 1, "No Source specified; cannot look up fetch method.\n"); } if (!tmp_error) { dataType = getBlxDataType(0, source->str, keyFile, &tmp_error); if (!dataType && !tmp_error) g_set_error(&tmp_error, BLX_ERROR, 1, "No data-type found for source '%s'\n", source->str); } if (!tmp_error) { if (dataType->bulkFetch) { GQuark fetchMethodQuark = g_array_index(dataType->bulkFetch, GQuark, 0); fetchMethod = getFetchMethodDetails(fetchMethodQuark, bc->fetchMethods); } if (!fetchMethod) { g_set_error(&tmp_error, BLX_ERROR, 1, "No fetch method specified for data-type '%s'\n", g_quark_to_string(dataType->name)); } /* The output of the fetch must be a natively supported file format (i.e. GFF) */ if (!tmp_error && fetchMethod->outputType != BLXFETCH_OUTPUT_GFF) { g_set_error(&tmp_error, BLX_ERROR, 1, "Expected fetch method output type to be '%s' but got '%s'\n", outputTypeStr(BLXFETCH_OUTPUT_GFF), outputTypeStr(fetchMethod->outputType)); } } if (!tmp_error) { MatchSequenceData match_data = {NULL, bc->refSeqName, start, end, bc->dataset, source->str, filename}; GString *command = doGetFetchCommand(fetchMethod, &match_data, &tmp_error); if (!tmp_error && command && command->str) { const char *fetchName = g_quark_to_string(fetchMethod->name); GSList *styles = blxReadStylesFile(NULL, NULL); sendFetchOutputToFile(command, keyFile, &bc->blastMode, bc->featureLists, bc->supportedTypes, styles, &bc->matchSeqs, &bc->mspList, fetchName, bc->saveTempFiles, newMsps, newSeqs, bc->columnList, lookupTable, refSeqOffset, refSeqRange, &tmp_error); } } if (tmp_error) g_propagate_error(error, tmp_error); } /* Dynamically load in additional features from a file. (should be called after * blixem's GUI has already started up, rather than during start-up where normal * feature-loading happens) */ static void dynamicLoadFeaturesFile(GtkWidget *blxWindow, const char *filename, const char *buffer, GError **error) { /* Must be passed either a filename or buffer */ if (!filename && !buffer) return; BlxViewContext *bc = blxWindowGetContext(blxWindow); GKeyFile *keyFile = blxGetConfig(); /* We'll load the features from the file into some temporary lists */ MSP *newMsps = NULL; GList *newSeqs = NULL; GError *tmp_error = NULL; int numAdded = 0; /* Create a temporary lookup table for BlxSequences so we can link them on GFF ID */ GHashTable *lookupTable = g_hash_table_new(g_direct_hash, g_direct_equal); /* Assume it's a natively-supported file and attempt to parse it. The first thing this * does is check that it's a native file and if not it sets the error */ loadNativeFile(filename, buffer, keyFile, &bc->blastMode, bc->featureLists, bc->supportedTypes, bc->styles, &newMsps, &newSeqs, bc->columnList, lookupTable, bc->refSeqOffset, &bc->refSeqRange, &tmp_error); if (tmp_error && filename) { /* Input file is not natively supported. We can still load it if * there is a fetch method associated with it: ask the user what * the Source is so that we can find the fetch method. Probably * should only get here if the input is an actual file so don't * support this for buffers for now. */ g_error_free(tmp_error); tmp_error = NULL; loadNonNativeFile(filename, blxWindow, &newMsps, &newSeqs, lookupTable, bc->refSeqOffset, &bc->refSeqRange, &tmp_error); } if (!tmp_error) { /* Count how many features were added. (Need to do this before blxMergeFeatures because * once this list gets merged the count will no longer be correct.) */ numAdded = g_list_length(newSeqs); /* Fetch any missing sequence data and finalise the new sequences */ bulkFetchSequences(0, FALSE, bc->saveTempFiles, bc->seqType, &newSeqs, bc->columnList, bc->bulkFetchDefault, bc->fetchMethods, &newMsps, &bc->blastMode, bc->featureLists, bc->supportedTypes, NULL, bc->refSeqOffset, &bc->refSeqRange, bc->dataset, FALSE, lookupTable); } if (newMsps) { finaliseFetch(newSeqs, bc->columnList); finaliseBlxSequences(bc->featureLists, &newMsps, &newSeqs, bc->columnList, bc->refSeqOffset, bc->seqType, bc->numFrames, &bc->refSeqRange, TRUE, lookupTable); double lowestId = calculateMspData(newMsps, bc); bigPictureSetMinPercentId(blxWindowGetBigPicture(blxWindow), lowestId); /* Add the msps/sequences to the tree data models (must be done after finalise because * finalise populates the child msp lists for parent features) */ detailViewAddMspData(blxWindowGetDetailView(blxWindow), newMsps, newSeqs); /* Merge the temporary lists into the main lists (takes ownership of the temp lists) */ blxMergeFeatures(newMsps, newSeqs, &bc->mspList, &bc->matchSeqs); /* Cache the new msp display ranges and sort and filter the trees. */ GtkWidget *detailView = blxWindowGetDetailView(blxWindow); cacheMspDisplayRanges(bc, detailViewGetNumUnalignedBases(detailView)); detailViewResortTrees(detailView); callFuncOnAllDetailViewTrees(detailView, refilterTree, NULL); /* Recalculate the coverage */ calculateDepth(bc); updateCoverageDepth(blxWindowGetCoverageView(blxWindow), bc); /* Re-calculate the height of the exon views */ GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); calculateExonViewHeight(bigPictureGetFwdExonView(bigPicture)); calculateExonViewHeight(bigPictureGetRevExonView(bigPicture)); forceResize(bigPicture); blxWindowRedrawAll(blxWindow); if (numAdded == 0) g_warning("No features loaded\n"); else if (numAdded == 1) g_message("Loaded %d new feature\n", numAdded); else g_message("Loaded %d new features\n", numAdded); } g_hash_table_unref(lookupTable); if (tmp_error) g_propagate_error(error, tmp_error); } /*********************************************************** * View panes menu * ***********************************************************/ /* Toggle visibility the n'th tree. This is the active strand's frame n if displaying * protein matches (where we only display one strand), or the forward or reverse * strand tree if displaying DNA matches (where both strands are displayed). */ static void toggleTreeVisibility(GtkWidget *blxWindow, const int number) { const gboolean toggled = blxWindowGetDisplayRev(blxWindow); const BlxStrand activeStrand = toggled ? BLXSTRAND_REVERSE : BLXSTRAND_FORWARD; /* For protein matches, trees are always displayed in frame order (i.e. 1, 2, 3), * so just use the number pressed for the frame, and the active strand for the * strand. */ int frame = number; BlxStrand strand = activeStrand; /* For DNA matches, the frame is always 1, but the strand depends on which number * was pressed: use 1 to toggle active strand, 2 for other strand */ if (blxWindowGetSeqType(blxWindow) == BLXSEQ_DNA) { frame = 1; if (number == 1) { strand = activeStrand; } else if (number == 2) { strand = toggled ? BLXSTRAND_FORWARD : BLXSTRAND_REVERSE; } } GtkWidget *detailView = blxWindowGetDetailView(blxWindow); GtkWidget *tree = detailViewGetTreeContainer(detailView, strand, frame); if (tree && gtk_widget_get_parent(tree)) { widgetSetHidden(tree, !widgetGetHidden(tree)); } } /* Toggle visibility of the active (1) or other (2) strand grid depending on the number pressed */ static void toggleGridVisibility(GtkWidget *blxWindow, const int number) { if (number == 1 || number == 2) { GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); const gboolean useFwdGrid = (number == 1) != blxWindowGetDisplayRev(blxWindow); GtkWidget *grid = useFwdGrid ? bigPictureGetFwdGrid(bigPicture) : bigPictureGetRevGrid(bigPicture); widgetSetHidden(grid, !widgetGetHidden(grid)); /* We need to force a resize of the big picture because the size-allocate * signal doesn't get emitted by default when its contents shrink */ forceResize(bigPicture); } } /* Toggle visibility of the active (1) or other (2) strand exon view depending on the number pressed */ static void toggleExonViewVisibility(GtkWidget *blxWindow, const int number) { if (number == 1 || number == 2) { GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); const gboolean useFwdExonView = (number == 1) != blxWindowGetDisplayRev(blxWindow); GtkWidget *exonView = useFwdExonView ? bigPictureGetFwdExonView(bigPicture) : bigPictureGetRevExonView(bigPicture); widgetSetHidden(exonView, !widgetGetHidden(exonView)); forceResize(bigPicture); } } /* Toggle the visibility of tree/grid panes following a number key press */ static void togglePaneVisibility(GtkWidget *blxWindow, const int number, const gboolean modifier1, const gboolean modifier2) { /* Affects big picture if modifier1 was pressed, the detail view otherwise */ if (modifier1) { /* If modifier 2 was also pressed, affects the exon views; otherwise the grids */ if (modifier2) { toggleExonViewVisibility(blxWindow, number); } else { toggleGridVisibility(blxWindow, number); } } else { toggleTreeVisibility(blxWindow, number); } } /* Repeat the last find operation. Searches for the next (rightwards) match unless the given * modifier is pressed, in which case it searches for the previous (leftwards) match */ static void findAgain(GtkWidget *blxWindow, const gboolean modifier) { GError *error = NULL; const int startCoord = getSearchStartCoord(blxWindow, FALSE, modifier); /* Try the DNA search. Does nothing if last search was not a DNA search. */ blxWindowFindDnaString(blxWindow, NULL, startCoord, modifier, TRUE, &error); if (error) { /* DNA search was attempted but not found. Try looping round to the beginning */ g_error_free(error); error = NULL; const int newStart = getSearchStartCoord(blxWindow, TRUE, modifier); blxWindowFindDnaString(blxWindow, NULL, newStart, modifier, TRUE, &error); } if (!error) { /* Try the search-from-list search. Returns NULL if last search was not a list search */ GList *seqList = findSeqsFromList(blxWindow, NULL, BLXCOL_NONE, FALSE, TRUE, &error); if (!seqList && !error) { /* Try the search-by-name search. Returns NULL if last search was not a name search. */ seqList = findSeqsFromColumn(blxWindow, NULL, BLXCOL_NONE, FALSE, TRUE, &error); } /* If either the list or name search succeeded, select the prev/next MSP from the * found sequence(s) depending on which direction we're searching. */ if (seqList) { blxWindowSetSelectedSeqList(blxWindow, seqList); if (modifier) { prevMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } else { nextMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } } } if (error) { prefixError(error, "Find %s failed. ", (modifier ? "previous" : "next")); reportAndClearIfError(&error, G_LOG_LEVEL_MESSAGE); } } /* Called when the state of a check button is toggled */ static void onVisibilityButtonToggled(GtkWidget *button, gpointer data) { GtkWidget *widgetToToggle = GTK_WIDGET(data); gboolean visible = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); widgetSetHidden(widgetToToggle, !visible); } /* Create a check button to control visibility of the given widget */ static void createVisibilityButton(GtkWidget *widgetToToggle, const char *mnemonic, GtkWidget *container) { GtkWidget *button = gtk_check_button_new_with_mnemonic(mnemonic); gtk_container_add(GTK_CONTAINER(container), button); /* Set the state depending on the widget's current visibility */ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), GTK_WIDGET_VISIBLE(widgetToToggle)); g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(onVisibilityButtonToggled), widgetToToggle); } /* Create a check button to control visibility of the given tree. */ static void createTreeVisibilityButton(GtkWidget *detailView, const BlxStrand strand, const int frame, GtkWidget *container) { /* Some trees may have been removed from the blixem window if they are not on the active * strand, so only show check boxes for those that are in the window (i.e. have a parent). * Note that we get the tree container here, which consists of the tree itself plus any headers etc. */ GtkWidget *tree = detailViewGetTreeContainer(detailView, strand, frame); if (gtk_widget_get_parent(tree)) { const gboolean toggled = detailViewGetDisplayRev(detailView); gboolean isActiveStrand = ((strand == BLXSTRAND_FORWARD) != toggled); if (detailViewGetSeqType(detailView) == BLXSEQ_DNA) { /* We only have 1 frame, but trees are from both strands, so distinguish between strands. * Put each strand in its own frame. */ char text1[] = "Show _active strand"; char text2[] = "Show othe_r strand"; GtkWidget *frame = gtk_frame_new(isActiveStrand ? "Active strand" : "Other strand"); gtk_container_add(GTK_CONTAINER(container), frame); createVisibilityButton(tree, isActiveStrand ? text1 : text2, frame); } else { /* All the visible trees should be in the same strand, so just distinguish by frame number. */ char formatStr[] = "Show frame _%d"; char displayText[strlen(formatStr) + numDigitsInInt(frame) + 1]; sprintf(displayText, formatStr, frame); createVisibilityButton(tree, displayText, container); } } } /* Callback called when the user clicks the 'bump exon view' button */ static void onBumpExonView(GtkWidget *button, gpointer data) { GtkWidget *exonView = GTK_WIDGET(data); const gboolean expanded = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); exonViewSetExpanded(exonView, expanded); } /* Create the set of settings buttons to control display of an exon-view widget. */ static void createExonButtons(GtkWidget *exonView, const char *visLabel, const char *bumpLabel, GtkWidget *parent) { /* Pack everything in an hbox */ GtkWidget *hbox = gtk_hbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(parent), hbox); /* Create a check button to control visibility of the exon view */ createVisibilityButton(exonView, visLabel, hbox); /* Create a check button to control whether the exon view is expanded or compressed */ const gboolean isBumped = exonViewGetExpanded(exonView); createCheckButton(GTK_BOX(hbox), bumpLabel, isBumped, G_CALLBACK(onBumpExonView), exonView); } /* Shows the "View panes" dialog. This dialog allows the user to show/hide certain portions of the window. */ void showViewPanesDialog(GtkWidget *blxWindow, const gboolean bringToFront) { BlxViewContext *bc = blxWindowGetContext(blxWindow); const BlxDialogId dialogId = BLXDIALOG_VIEW; GtkWidget *dialog = getPersistentDialog(bc->dialogList, dialogId); if (!dialog) { char *title = g_strdup_printf("%sView panes", blxGetTitlePrefix(bc)); dialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(blxWindow), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL); g_free(title); /* These calls are required to make the dialog persistent... */ addPersistentDialog(bc->dialogList, dialogId, dialog); g_signal_connect(dialog, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(dialog, "response", G_CALLBACK(onResponseDialog), GINT_TO_POINTER(TRUE)); } else { /* Clear contents and re-create */ dialogClearContentArea(GTK_DIALOG(dialog)); } gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); GtkWidget *contentArea = GTK_DIALOG(dialog)->vbox; int borderWidth = 12; /* Big picture */ GtkWidget *bp = blxWindowGetBigPicture(blxWindow); GtkWidget *bpVbox = createVBoxWithBorder(contentArea, borderWidth, TRUE, "Big picture"); createVisibilityButton(bp, "Show _big picture", bpVbox); GtkWidget *bpSubBox = createVBoxWithBorder(bpVbox, borderWidth, FALSE, NULL); GtkWidget *bpActiveStrand = createVBoxWithBorder(bpSubBox, 0, TRUE, "Active strand"); createVisibilityButton(bigPictureGetActiveGrid(bp), "Show _grid", bpActiveStrand); createExonButtons(bigPictureGetActiveExonView(bp), "Show _exons ", "_Bump exons ", bpActiveStrand); GtkWidget *bpOtherStrand = createVBoxWithBorder(bpSubBox, 0, TRUE, "Other strand"); createVisibilityButton(bigPictureGetInactiveGrid(bp), "Show gr_id", bpOtherStrand); createExonButtons(bigPictureGetInactiveExonView(bp), "Show e_xons ", "Bum_p exons ", bpOtherStrand); /* Detail view */ GtkWidget *dvVbox = createVBoxWithBorder(contentArea, borderWidth, TRUE, "Alignment lists"); GtkWidget *dv = blxWindowGetDetailView(blxWindow); createVisibilityButton(dv, "Show alignment _lists", dvVbox); GtkWidget *dvSubBox = createVBoxWithBorder(dvVbox, borderWidth, FALSE, NULL); const int numFrames = blxWindowGetNumFrames(blxWindow); int frame = 1; for ( ; frame <= numFrames; ++frame) { createTreeVisibilityButton(dv, blxWindowGetActiveStrand(blxWindow), frame, dvSubBox); createTreeVisibilityButton(dv, blxWindowGetInactiveStrand(blxWindow), frame, dvSubBox); } /* Coverage view */ GtkWidget *coverageView = bigPictureGetCoverageView(bp); GtkWidget *coverageVbox = createVBoxWithBorder(contentArea, borderWidth, TRUE, "Coverage view"); createVisibilityButton(coverageView, "Show _coverage view", coverageVbox); gtk_widget_show_all(dialog); if (bringToFront) { gtk_window_present(GTK_WINDOW(dialog)); } } /*********************************************************** * Find menu * ***********************************************************/ static GList* findSeqsFromColumn(GtkWidget *blxWindow, const char *inputText, const BlxColumnId inputCol, const gboolean rememberSearch, const gboolean findAgain, GError **error) { /* Previous values (if applicable) */ static char *prevSearchStr = NULL; static BlxColumnId prevSearchCol = BLXCOL_NONE; /* Current values */ char *searchStr = NULL; BlxColumnId searchCol = BLXCOL_NONE; /* If it's a find-again, use the existing values; otherwise, use the input values */ if (findAgain) { searchStr = prevSearchStr; searchCol = prevSearchCol; } else { g_free(searchStr); searchStr = g_strdup(inputText); searchCol = inputCol; if (rememberSearch) { prevSearchStr = searchStr; prevSearchCol = searchCol; } } if (!searchStr || searchCol == BLXCOL_NONE) { /* We will get here if we do a find-again when there wasn't a previous find */ return NULL; } /* Loop through all the sequences and see if the sequence data for this column * matches the search string */ GList *seqList = blxWindowGetAllMatchSeqs(blxWindow); BlxViewContext *bc = blxWindowGetContext(blxWindow); SeqSearchData searchData = {searchStr, searchCol, bc, NULL, NULL}; g_list_foreach(seqList, getSequencesThatMatch, &searchData); if (g_list_length(searchData.matchList) < 1) { GList *columnList = blxWindowGetColumnList(blxWindow); const char *columnName = getColumnTitle(columnList, searchCol); if (searchData.error) g_propagate_error(error, searchData.error); else g_set_error(error, BLX_ERROR, BLX_ERROR_STRING_NOT_FOUND, "No sequences found where column '%s' matches text '%s'.\n", columnName, searchStr); } return searchData.matchList; } /* Utility to extract the contents of a GtkTextView and return it as a string. The result is * owned by the GtkTextView and should not be free'd. */ static const char* getStringFromTextView(GtkTextView *textView) { if (!textView || !GTK_WIDGET_SENSITIVE(GTK_WIDGET(textView))) { g_critical("Could not set search string: invalid text entry box\n"); return NULL; } /* Get the input text from the text buffer and create the group */ GtkTextBuffer *textBuffer = gtk_text_view_get_buffer(textView); GtkTextIter start, end; gtk_text_buffer_get_bounds(textBuffer, &start, &end); return gtk_text_buffer_get_text(textBuffer, &start, &end, TRUE); } /* Finds all the valid sequences blixem knows about whose column data matches * an item from the given input text. Returns the results as a GList of BlxSequences. * The input text may be a multi-line list (one search item per line). */ static GList* findSeqsFromList(GtkWidget *blxWindow, const char *inputText, const BlxColumnId inputCol, const gboolean rememberSearch, const gboolean findAgain, GError **error) { /* Previous values (if applicable) */ static char *prevSearchStr = NULL; static BlxColumnId prevSearchCol = BLXCOL_NONE; /* Current values */ char *searchStr = NULL; BlxColumnId searchCol = BLXCOL_NONE; /* If we-re doing a find-again, use the values from last time; otherwise use the input values */ if (findAgain) { searchStr = prevSearchStr; searchCol = prevSearchCol; } else { g_free(searchStr); searchStr = g_strdup(inputText); searchCol = inputCol; if (rememberSearch) { prevSearchStr = searchStr; prevSearchCol = searchCol; } } if (!searchStr || searchCol == BLXCOL_NONE) { /* We may get here if we did a find-again when there was no previous find */ return NULL; } GError *tmpError = NULL; GList *seqList = getSeqStructsFromText(blxWindow, searchStr, searchCol, &tmpError); if (g_list_length(seqList) < 1) { GList *columnList = blxWindowGetColumnList(blxWindow); const char *columnName = getColumnTitle(columnList, searchCol); if (tmpError) g_propagate_error(error, tmpError); else g_set_error(error, BLX_ERROR, BLX_ERROR_STRING_NOT_FOUND, "No sequences found where column '%s' matches text '%s'.\n", columnName, searchStr); } return seqList; } /* Callback called when requested to find sequences from a sequence name. Selects * the sequences and scrolls to the start of the first match in the selection */ static gboolean onFindSeqsFromName(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; const char *inputText = NULL; if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) { inputText = getStringFromTextEntry(GTK_ENTRY(data)); } GtkWidget *dialog = gtk_widget_get_toplevel(button); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(GTK_WINDOW(dialog))); /* Find the combo box on the dialog (there should only be one), which tells * us which column to search. */ GtkComboBox *combo = widgetGetComboBox(dialog); BlxColumnId searchCol = getColumnFromComboBox(combo); /* Find all sequences that match */ GError *error = NULL; GList *seqList = findSeqsFromColumn(blxWindow, inputText, searchCol, TRUE, FALSE, &error); if (error) { reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL); result = FALSE; } if (seqList) { GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); blxWindowSetSelectedSeqList(blxWindow, seqList); if (responseId == BLX_RESPONSE_FORWARD) { nextMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } else if (responseId == BLX_RESPONSE_BACK) { prevMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } else { firstMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } } return result; } /* Callback called when requested to find sequences from a given list. Selects * the sequences ands scrolls to the start of the first match in the selection. */ static gboolean onFindSeqsFromList(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; const char *inputText = NULL; /* Nothing to do if this button is not active */ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) { inputText = getStringFromTextView(GTK_TEXT_VIEW(data)); } /* Get the dialog and main window */ GtkWidget *dialog = gtk_widget_get_toplevel(button); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(GTK_WINDOW(dialog))); /* Find the combo box on the dialog (there should only be one), which tells * us which column to search. */ GtkComboBox *combo = widgetGetComboBox(dialog); BlxColumnId searchCol = getColumnFromComboBox(combo); GError *error = NULL; GList *seqList = findSeqsFromList(blxWindow, inputText, searchCol, TRUE, FALSE, &error); if (error) { reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL); result = FALSE; } if (seqList) { blxWindowSetSelectedSeqList(blxWindow, seqList); if (responseId == BLX_RESPONSE_FORWARD) { nextMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } else if (responseId == BLX_RESPONSE_BACK) { prevMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } else { firstMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } } return result; } /* Search for the given DNA string in the reference sequence. Searches for the next (rightwards) * value from the given start coord, unless searchLeft is true in which case it searches leftwards. * If findAgain is true it repeats the last DNA search. */ static void blxWindowFindDnaString(GtkWidget *blxWindow, const char *inputSearchStr, const int refSeqStart, const gboolean searchLeft, const gboolean findAgain, GError **error) { /* Remember the last input string for use with findAgain */ static char *searchStr = NULL; if (!findAgain) { /* We must copy the input string because it may not exist if/when we come to do a 'find again' */ g_free(searchStr); searchStr = g_strdup(inputSearchStr); } const int searchStrMax = searchStr ? strlen(searchStr) - 1 : -1; if (searchStrMax < 0) { return; } const int searchStart = searchLeft ? searchStrMax : 0; const int searchEnd = searchLeft ? 0 : searchStrMax; const int searchStrIncrement = searchLeft ? -1 : 1; /* Values increase left-to-right in normal display or right-to-left in reversed display */ BlxViewContext *bc = blxWindowGetContext(blxWindow); const gboolean searchForward = (searchLeft == bc->displayRev); const int refSeqIncrement = searchForward ? 1 : -1; /* We'll need to complement ref seq bases if the active strand is the reverse strand */ const gboolean complement = (blxWindowGetActiveStrand(blxWindow) == BLXSTRAND_REVERSE); int refSeqIdx = refSeqStart; int searchStrIdx = searchStart; int matchStart = UNSET_INT; while (refSeqIdx >= bc->refSeqRange.min && refSeqIdx <= bc->refSeqRange.max && searchStrIdx >= 0 && searchStrIdx <= searchStrMax) { const char refSeqBase = getSequenceIndex(bc->refSeq, refSeqIdx, complement, &bc->refSeqRange, BLXSEQ_DNA); char searchStrBase = convertBaseToCorrectCase(searchStr[searchStrIdx], BLXSEQ_DNA); if (refSeqBase == searchStrBase) { /* The base matches. If it's the first matching base, set the match-start coord (or if we're * searching leftwards, then always set the match-start coord, because the start is actually * the last coord that will be found). Then proceed to the next position in the search string */ if (matchStart == UNSET_INT) { matchStart = refSeqIdx; } searchStrIdx += searchStrIncrement; refSeqIdx += refSeqIncrement; } else if (matchStart != UNSET_INT) { /* We were in a match but this base doesn't match. Reset to the start of the * search string, and start looking again from one base after the place where the last * match started. (We need to re-check all bases from there because we're comparing * against a different section of the search string now.) */ searchStrIdx = searchStart; refSeqIdx = matchStart + refSeqIncrement; matchStart = UNSET_INT; } else { refSeqIdx += refSeqIncrement; } } /* Undo the last increment, so that we have the final coords of the matching section (if found) */ refSeqIdx -= refSeqIncrement; searchStrIdx -= searchStrIncrement; /* If we reached the end of the search string, then we matched the whole lot. */ const gboolean finished = searchStrIdx == searchEnd; if (matchStart != UNSET_INT && finished) { GtkWidget *detailView = blxWindowGetDetailView(blxWindow); const int frame = 1; int resultStart = searchLeft ? refSeqIdx : matchStart; int resultEnd = searchLeft ? matchStart : refSeqIdx; /* Select the start index in the result */ detailViewSetSelectedDnaBaseIdx(detailView, resultStart, frame, TRUE, FALSE, FALSE); /* Extend the selection to the end index */ detailViewSetSelectedDnaBaseIdx(detailView, resultEnd, frame, FALSE, TRUE, TRUE); detailViewRedrawAll(detailView); } else { g_set_error(error, BLX_ERROR, BLX_ERROR_STRING_NOT_FOUND, "The string '%s' was not found in the reference sequence searching to the %s from coord %d.\n", searchStr, (searchLeft ? "left" : "right"), refSeqStart); } } /* Get the start coord for a search. If startBeginning is false, this gets the currently-selected display * index (shifted by one base so that we don't start searching at the same position as a previous * find result) or, if no base index is selected, returns the start coord of the current display range. If * startBeginning is true, just start from the beginning of the reference sequence. The result is * nucleotide coord on the ref sequence. */ static int getSearchStartCoord(GtkWidget *blxWindow, const gboolean startBeginning, const gboolean searchLeft) { int result = UNSET_INT; const BlxViewContext *bc = blxWindowGetContext(blxWindow); if (startBeginning) { result = (searchLeft == bc->displayRev) ? bc->refSeqRange.min : bc->refSeqRange.max; } else { GtkWidget *detailView = blxWindowGetDetailView(blxWindow); result = detailViewGetSelectedDisplayIdx(detailView); if (result != UNSET_INT) { /* Increment by one to make sure we don't re-find a previously-found match * (or decrement if searching leftwards) */ if (searchLeft) { --result; } else { ++result; } } else { /* The start display coord is the min coord if we're searching left and the max if searching right. */ const IntRange* const displayRange = detailViewGetDisplayRange(detailView); result = searchLeft ? displayRange->max : displayRange->min; } /* Convert the display coord to a nucleotide coord */ result = convertDisplayIdxToDnaIdx(result, bc->seqType, 1, 1, bc->numFrames, bc->displayRev, &bc->refSeqRange); } return result; } /* Callback called when requested to search for a DNA string. If found, sets the currently- * selected base index to the coord where the matching string starts. The text entry for the * search string is passed as the callback data. */ static gboolean onFindDnaString(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; /* Get the search string from the text entry. If the toggle button is not active, call * blxWindowFindDnaString with a NULL search string to "cancel" any previous searches * so that "findAgain" will not attempt to perform a DNA search). */ const char *searchStr = NULL; if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) { searchStr = getStringFromTextEntry(GTK_ENTRY(data)); if (!searchStr || strlen(searchStr) < 1) { g_critical("DNA search failed. The search string was empty.\n"); result = FALSE; } } /* Search left wrt the screen if the user hit 'back' search right if 'forward' */ const gboolean searchLeft = (responseId == BLX_RESPONSE_BACK); GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); const gboolean startBeginning = (responseId != BLX_RESPONSE_FORWARD && responseId != BLX_RESPONSE_BACK); int startCoord = getSearchStartCoord(blxWindow, startBeginning, searchLeft); GError *error = NULL; blxWindowFindDnaString(blxWindow, searchStr, startCoord, searchLeft, FALSE, &error); if (error) { if (!startBeginning) { /* Try looping round to the beginning */ postfixError(error, " Trying again from the %s of the range.\n", (searchLeft ? "end" : "start")); reportAndClearIfError(&error, G_LOG_LEVEL_WARNING); startCoord = getSearchStartCoord(blxWindow, TRUE, searchLeft); blxWindowFindDnaString(blxWindow, searchStr, startCoord, searchLeft, FALSE, &error); } } if (error) { result = FALSE; prefixError(error, "DNA search failed. "); reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL); } return result; } /* Utility to create a drop-down combo box for selecting the column to search by */ static void createSearchColumnCombo(GtkTable *table, const int col, const int row, GtkWidget *blxWindow) { GtkWidget *hbox = gtk_hbox_new(FALSE, 0); gtk_table_attach(table, hbox, col, col + 1, row, row + 1, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, DEFAULT_TABLE_XPAD, DEFAULT_TABLE_YPAD); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); DetailViewProperties *dvProperties = detailViewGetProperties(detailView); GList *columnList = blxWindowGetColumnList(dvProperties->blxWindow); createSortBox(GTK_BOX(hbox), detailView, BLXCOL_SEQNAME, columnList, "Search column: ", TRUE); } static void onClearFindDialog(GtkWidget *button, gpointer data) { GSList *entryList = (GSList*)data; for ( ; entryList; entryList = entryList->next) { if (GTK_IS_ENTRY(entryList->data)) { GtkEntry *entry = GTK_ENTRY(entryList->data); gtk_entry_set_text(entry, ""); } else if (GTK_IS_TEXT_VIEW(entryList->data)) { GtkTextView *textView = GTK_TEXT_VIEW(entryList->data); gtk_text_buffer_set_text(gtk_text_view_get_buffer(textView), "", -1); } else { g_warning("onClearFindDialog: Unexpected widget type: expected GtkEntry or GtkTextView\n"); } } } /* Clear up data created for the find dialog when it is destroyed * (here for completeness but not actually called because the dialog * is persistent). */ static void onDestroyFindDialog(GtkWidget *widget, gpointer data) { GSList *entryList = (GSList*)data; if (entryList) { g_slist_free(entryList); } } /* Show the 'Find' dialog */ void showFindDialog(GtkWidget *blxWindow, const gboolean bringToFront) { BlxViewContext *bc = blxWindowGetContext(blxWindow); const BlxDialogId dialogId = BLXDIALOG_FIND; GtkWidget *dialog = getPersistentDialog(bc->dialogList, dialogId); if (!dialog) { char *title = g_strdup_printf("%sFind sequences", blxGetTitlePrefix(bc)); /* Note that we add some buttons here but some more at the end because * we want to create a custom Clear button in the middle somewhere */ dialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(blxWindow), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_GO_BACK, /* previous match */ BLX_RESPONSE_BACK, GTK_STOCK_GO_FORWARD, /* next match */ BLX_RESPONSE_FORWARD, NULL); g_free(title); /* These calls are required to make the dialog persistent... */ addPersistentDialog(bc->dialogList, dialogId, dialog); g_signal_connect(dialog, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); GtkBox *contentArea = GTK_BOX(GTK_DIALOG(dialog)->vbox); GtkBox *actionArea = GTK_BOX(GTK_DIALOG(dialog)->action_area); const int numRows = 3; const int numCols = 2; GtkTable *table = GTK_TABLE(gtk_table_new(numRows, numCols, FALSE)); gtk_box_pack_start(contentArea, GTK_WIDGET(table), TRUE, TRUE, 0); /* This list will be populated with the text entry widgets. */ GSList *entryList = NULL; /* Column 1: match-seq search options */ GtkRadioButton *button1 = createRadioButton(table, 1, 1, NULL, "_Text search (wildcards * and ?)", TRUE, TRUE, FALSE, onFindSeqsFromName, blxWindow, &entryList); createRadioButton(table, 1, 2, button1, "_List search", FALSE, TRUE, TRUE, onFindSeqsFromList, blxWindow, &entryList); createSearchColumnCombo(table, 1, 3, blxWindow); /* Column 2: ref-seq search options */ createRadioButton(table, 2, 1, button1, "_DNA search", FALSE, TRUE, FALSE, onFindDnaString, blxWindow, &entryList); /* Add a button to clear the text entry fields. It's easier to do this * here than in the response callback because we want to send different data */ GtkWidget *clearButton = gtk_button_new_from_stock(GTK_STOCK_CLEAR); g_signal_connect(G_OBJECT(clearButton), "clicked", G_CALLBACK(onClearFindDialog), entryList); gtk_box_pack_end(actionArea, clearButton, FALSE, FALSE, 0); /* Add remaining buttons after the Clear button */ gtk_dialog_add_buttons(GTK_DIALOG(dialog), GTK_STOCK_CLOSE, /* close / cancel */ GTK_RESPONSE_REJECT, GTK_STOCK_OK, /* ok, do the search */ GTK_RESPONSE_ACCEPT, NULL); gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(blxWindow)); g_signal_connect(dialog, "response", G_CALLBACK(onResponseDialog), GINT_TO_POINTER(TRUE)); g_signal_connect(dialog, "destroy", G_CALLBACK(onDestroyFindDialog), entryList); } gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); gtk_widget_show_all(dialog); if (bringToFront) { gtk_window_present(GTK_WINDOW(dialog)); } } /* Show the 'Info' dialog, which displays info about the currently-selected sequence(s) */ void showInfoDialog(GtkWidget *blxWindow) { GtkWidget *dialog = gtk_dialog_new_with_buttons("Blixem - Sequence info", NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CLOSE, GTK_RESPONSE_REJECT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT); int width = blxWindow->allocation.width * 0.7; int height = blxWindow->allocation.height * 0.9; BlxViewContext *bc = blxWindowGetContext(blxWindow); /* Compile the message text from the selected sequence(s) */ GString *resultStr = g_string_new(""); GList *seqItem = bc->selectedSeqs; for ( ; seqItem; seqItem = seqItem->next) { BlxSequence *blxSeq = (BlxSequence*)(seqItem->data); char *seqText = blxSequenceGetInfo(blxSeq, TRUE, bc->columnList); g_string_append_printf(resultStr, "%s\n\n", seqText); g_free(seqText); } /* We'll use the same fixed-width font as the detail-view */ GtkWidget *detailView = blxWindowGetDetailView(blxWindow); PangoFontDescription *fontDesc = detailViewGetFontDesc(detailView); GtkWidget *child = createScrollableTextView(resultStr->str, TRUE, fontDesc, TRUE, NULL, &height, NULL); gtk_window_set_default_size(GTK_WINDOW(dialog), width, height); gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), child, TRUE, TRUE, 0); g_signal_connect(dialog, "response", G_CALLBACK(onResponseDialog), NULL); gtk_widget_show_all(dialog); g_string_free(resultStr, TRUE); } /* Toggle the bump state. Currently only the exon view can be bumped, so this just * affects that. */ static void toggleBumpState(GtkWidget *blxWindow) { GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); exonViewToggleExpanded(bigPictureGetFwdExonView(bigPicture)); exonViewToggleExpanded(bigPictureGetRevExonView(bigPicture)); } /*********************************************************** * Group sequences menu * ***********************************************************/ /* Utility to free the given list of strings and (if the option is true) all * of its data items as well. */ static void freeStringList(GList **stringList, const gboolean freeDataItems) { if (stringList && *stringList) { if (freeDataItems) { GList *item = *stringList; for ( ; item; item = item->next) { char *strData = (char*)(item->data); g_free(strData); item->data = NULL; } } g_list_free(*stringList); *stringList = NULL; } } /* Free the memory used by the given sequence group and its members. */ static void destroySequenceGroup(BlxViewContext *bc, SequenceGroup **seqGroup) { if (seqGroup && *seqGroup) { /* Remove it from the list of groups */ bc->sequenceGroups = g_list_remove(bc->sequenceGroups, *seqGroup); /* If this is pointed to by the match-set pointer, null it */ if (*seqGroup == bc->matchSetGroup) { bc->matchSetGroup = NULL; } /* Free the memory used by the group name */ if ((*seqGroup)->groupName) { g_free((*seqGroup)->groupName); } /* Free the list of sequences */ if ((*seqGroup)->seqList) { freeStringList(&(*seqGroup)->seqList, (*seqGroup)->ownsSeqNames); } g_free(*seqGroup); *seqGroup = NULL; } } /* Delete a single group */ static void blxWindowDeleteSequenceGroup(GtkWidget *blxWindow, SequenceGroup *group) { BlxViewContext *blxContext = blxWindowGetContext(blxWindow); if (blxContext->sequenceGroups) { destroySequenceGroup(blxContext, &group); blxWindowGroupsChanged(blxWindow); } } /* Delete all groups */ static void blxContextDeleteAllSequenceGroups(BlxViewContext *bc) { GList *groupItem = bc->sequenceGroups; for ( ; groupItem; groupItem = groupItem->next) { SequenceGroup *group = (SequenceGroup*)(groupItem->data); destroySequenceGroup(bc, &group); } g_list_free(bc->sequenceGroups); bc->sequenceGroups = NULL; /* Reset the hide-not-in-group flags otherwise we'll hide everything! */ bc->flags[BLXFLAG_HIDE_UNGROUPED_SEQS] = FALSE ; bc->flags[BLXFLAG_HIDE_UNGROUPED_FEATURES] = FALSE ; } static void blxWindowDeleteAllSequenceGroups(GtkWidget *blxWindow) { BlxViewContext *bc = blxWindowGetContext(blxWindow); blxContextDeleteAllSequenceGroups(bc); blxWindowGroupsChanged(blxWindow); } /* Update function to be called whenever groups have been added or deleted, * or sequences have been added to or removed from a group */ static void blxWindowGroupsChanged(GtkWidget *blxWindow) { GtkWidget *detailView = blxWindowGetDetailView(blxWindow); GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); /* Re-sort all trees, because grouping affects sort order */ detailViewResortTrees(detailView); /* Refilter the trees (because groups affect whether sequences are visible) */ callFuncOnAllDetailViewTrees(detailView, refilterTree, NULL); /* Resize exon view because transcripts may have been hidden/unhidden */ calculateExonViewHeight(bigPictureGetFwdExonView(bigPicture)); calculateExonViewHeight(bigPictureGetRevExonView(bigPicture)); forceResize(bigPicture); /* Redraw all (because highlighting affects both big picture and detail view) */ blxWindowRedrawAll(blxWindow); } /* Create a new sequence group from the given list of sequence names, with a * unique ID and name, and add it to the blxWindow's list of groups. The group * should be destroyed with destroySequenceGroup. If ownSeqNames is true, the group * will take ownership of the sequence names and free them when it is destroyed. * Caller can optionally provide the group name; if not provided, a default name * will be allocated. */ static SequenceGroup* createSequenceGroup(GtkWidget *blxWindow, GList *seqList, const gboolean ownSeqNames, const char *groupName) { BlxViewContext *bc = blxWindowGetContext(blxWindow); /* Create the new group */ SequenceGroup *group = (SequenceGroup*)g_malloc(sizeof(SequenceGroup)); group->seqList = seqList; group->ownsSeqNames = ownSeqNames; group->hidden = FALSE; /* Find a unique ID */ GList *lastItem = g_list_last(bc->sequenceGroups); if (lastItem) { SequenceGroup *lastGroup = (SequenceGroup*)(lastItem->data); group->groupId = lastGroup->groupId + 1; } else { group->groupId = 1; } if (groupName) { group->groupName = g_strdup(groupName); } else { /* Create a default name based on the unique ID */ char formatStr[] = "Group%d"; const int nameLen = strlen(formatStr) + numDigitsInInt(group->groupId); group->groupName = (char*)g_malloc(nameLen * sizeof(*group->groupName)); sprintf(group->groupName, formatStr, group->groupId); } /* Set the order number. For simplicity, set the default order to be the same * as the ID number, so groups are sorted in the order they were added */ group->order = group->groupId; /* Set the default highlight color. */ group->highlighted = TRUE; BlxColorId colorId = (groupName && !strcmp(groupName, MATCH_SET_GROUP_NAME)) ? BLXCOLOR_MATCH_SET : BLXCOLOR_GROUP; GdkColor *color = getGdkColor(colorId, bc->defaultColors, FALSE, bc->usePrintColors); group->highlightColor = *color; /* Add it to the list, and update */ bc->sequenceGroups = g_list_append(bc->sequenceGroups, group); blxWindowGroupsChanged(blxWindow); return group; } /* This function sets the sequence-group-name text based on the given text entry widget */ static gboolean onGroupNameChanged(GtkWidget *widget, const gint responseId, gpointer data) { gboolean result = TRUE; GtkEntry *entry = GTK_ENTRY(widget); SequenceGroup *group = (SequenceGroup*)data; const gchar *newName = gtk_entry_get_text(entry); if (!newName || strlen(newName) < 1) { g_critical("Invalid group name '%s' entered; reverting to previous group name '%s'.", newName, group->groupName); gtk_entry_set_text(entry, group->groupName); result = FALSE; } else { if (group->groupName) g_free(group->groupName); group->groupName = g_strdup(newName); } return result; } /* This function is called when the sequence-group-order text entry widget's * value has changed. It sets the new order number in the group. */ static gboolean onGroupOrderChanged(GtkWidget *widget, const gint responseId, gpointer data) { gboolean result = TRUE; GtkEntry *entry = GTK_ENTRY(widget); SequenceGroup *group = (SequenceGroup*)data; const gchar *newOrder = gtk_entry_get_text(entry); if (!newOrder || strlen(newOrder) < 1) { g_critical("Invalid order number '%s' entered; reverting to previous order number '%d'.", newOrder, group->order); char *orderStr = convertIntToString(group->order); gtk_entry_set_text(entry, orderStr); g_free(orderStr); result = FALSE; } else { group->order = convertStringToInt(newOrder); GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(widget)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); blxWindowGroupsChanged(blxWindow); } return result; } /* This callback is called when the dialog settings are applied. It sets the hidden * status of the passed groupo based on the toggle button's state */ static gboolean onGroupHiddenToggled(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; SequenceGroup *group = (SequenceGroup*)data; group->hidden = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); /* Refilter trees and redraw all immediately show the new status */ GtkWidget *dialog = gtk_widget_get_toplevel(button); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(GTK_WINDOW(dialog))); blxWindowGroupsChanged(blxWindow); return result; } /* This callback is called when the toggle button for a group's "highlighted" flag is toggled. * It updates the group's highlighted flag according to the button's new status. */ static gboolean onGroupHighlightedToggled(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; SequenceGroup *group = (SequenceGroup*)data; group->highlighted = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); /* Redraw the blixem window to immediately show the new status. The toplevel * parent of the button is the dialog, and the blixem window is the transient * parent of the dialog. */ GtkWidget *dialog = gtk_widget_get_toplevel(button); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(GTK_WINDOW(dialog))); blxWindowRedrawAll(blxWindow); return result; } /* Called when the user has changed the color of a group in the 'edit groups' dialog */ static gboolean onGroupColorChanged(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; SequenceGroup *group = (SequenceGroup*)data; gtk_color_button_get_color(GTK_COLOR_BUTTON(button), &group->highlightColor); gdk_colormap_alloc_color(gdk_colormap_get_system(), &group->highlightColor, TRUE, TRUE); /* Redraw everything in the new colors */ GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); blxWindowRedrawAll(blxWindow); return result; } /* This function creates a widget that allows the user to edit the group * pointed to by the given list item, and adds it to the table container * widget at the given row. */ static void createEditGroupWidget(GtkWidget *blxWindow, SequenceGroup *group, GtkTable *table, const int row, const int xpad, const int ypad) { BlxViewContext *blxContext = blxWindowGetContext(blxWindow); /* Only show the special 'match set' group if it has some sequences */ if (group != blxContext->matchSetGroup || g_list_length(group->seqList) > 0) { /* Show the group's name in a text box that the user can edit */ GtkWidget *nameWidget = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(nameWidget), group->groupName); gtk_entry_set_activates_default(GTK_ENTRY(nameWidget), TRUE); widgetSetCallbackData(nameWidget, onGroupNameChanged, group); /* Add a check box for the 'hidden' flag */ GtkWidget *isHiddenWidget = gtk_check_button_new(); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(isHiddenWidget), group->hidden); widgetSetCallbackData(isHiddenWidget, onGroupHiddenToggled, group); /* Add a check box for the 'highlighted' flag */ GtkWidget *isHighlightedWidget = gtk_check_button_new(); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(isHighlightedWidget), group->highlighted); widgetSetCallbackData(isHighlightedWidget, onGroupHighlightedToggled, group); /* Show the group's order number in an editable text box */ GtkWidget *orderWidget = gtk_entry_new(); char *orderStr = convertIntToString(group->order); gtk_entry_set_text(GTK_ENTRY(orderWidget), orderStr); g_free(orderStr); gtk_entry_set_activates_default(GTK_ENTRY(orderWidget), TRUE); gtk_widget_set_size_request(orderWidget, 30, -1); widgetSetCallbackData(orderWidget, onGroupOrderChanged, group); /* Show the group's highlight color in a button that will also launch a color-picker */ GtkWidget *colorButton = gtk_color_button_new_with_color(&group->highlightColor); widgetSetCallbackData(colorButton, onGroupColorChanged, group); /* Create a button that will delete this group */ GtkWidget *deleteButton = gtk_button_new_from_stock(GTK_STOCK_DELETE); g_signal_connect(G_OBJECT(deleteButton), "clicked", G_CALLBACK(onButtonClickedDeleteGroup), group); /* Put everything in the table */ gtk_table_attach(table, nameWidget, 1, 2, row, row + 1, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, xpad, ypad); gtk_table_attach(table, isHiddenWidget, 2, 3, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, isHighlightedWidget, 3, 4, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, orderWidget, 4, 5, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, colorButton, 5, 6, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, deleteButton, 6, 7, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); } } /* Like blxSequenceGetColumn but also supports the group column (which * needs the context for its data) */ static const char* blxSequenceGetColumnData(const BlxSequence* const blxSeq, const BlxColumnId columnId, const BlxViewContext *bc) { const char *result = NULL; if (columnId == BLXCOL_GROUP) { const SequenceGroup *group = blxContextGetSequenceGroup(bc, blxSeq); if (group) result = group->groupName; } else { result = blxSequenceGetColumn(blxSeq, columnId); } return result; } /* Called for an entry from a list of BlxSequences. Compares the relevant data * from the BlxSequence (as indicated by the searchCol member of the SeqSearchData * struct) to the search string (also specified in the SeqSearchData). If it * matches, it appends the BlxSequence to the result list in the SeqSearchData. */ static void getSequencesThatMatch(gpointer listDataItem, gpointer data) { /* Get the BlxSequence for this list item */ BlxSequence *seq = (BlxSequence*)listDataItem; /* Get the search data */ SeqSearchData *searchData = (SeqSearchData*)data; if (searchData->error) return; /* already hit an error so don't try any more */ /* Get the relevant data for the search column */ const char *dataToCompare = blxSequenceGetColumnData(seq, searchData->searchCol, searchData->bc); if (!dataToCompare) { if (searchData->error) { /* Default error message is not that useful, so replace it */ reportAndClearIfError(&searchData->error, G_LOG_LEVEL_DEBUG); g_set_error(&searchData->error, BLX_ERROR, BLX_ERROR_INVALID_COLUMN, "Invalid search column.\n"); } return; } /* Do the search */ gboolean found = wildcardSearch(dataToCompare, searchData->searchStr); if (searchData->searchCol == BLXCOL_SEQNAME) { /* Sequence names have a variant number postfix; if not found, try * to match the text without this postfix. */ if (!found && dataToCompare) { char *seqName = g_strdup(dataToCompare); char *cutPoint = strchr(seqName, '.'); if (cutPoint) { *cutPoint = '\0'; found = wildcardSearch(seqName, searchData->searchStr); } g_free(seqName); } } if (found) { /* Add this BlxSequence onto the result list. */ searchData->matchList = g_list_prepend(searchData->matchList, seq); } } /* If the given radio button is enabled, add a group based on the curently- * selected sequences. */ static gboolean onAddGroupFromSelection(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) { GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); BlxViewContext *blxContext = blxWindowGetContext(blxWindow); if (g_list_length(blxContext->selectedSeqs) > 0) { GList *list = g_list_copy(blxContext->selectedSeqs); /* group takes ownership of this */ createSequenceGroup(blxWindow, list, FALSE, NULL); } else { result = FALSE; g_critical("Warning: cannot create group; no sequences are currently selected"); } } return result; } /* If the given radio button is enabled, add a group based on the search text * in the given text entry. This function finds the combo box on the dialog that * specifies which column to search by, and searches the relevant column for * the given search text. */ static gboolean onAddGroupFromText(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; /* Nothing to do if this radio button is not active */ if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) { return result; } /* Get the dialog and main window */ GtkWidget *dialog = gtk_widget_get_toplevel(button); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(GTK_WINDOW(dialog))); /* Get the search text from the text entry widget (which is passed as user data) */ const char *inputText = getStringFromTextEntry(GTK_ENTRY(data)); /* Find the combo box on the dialog (there should only be one), which tells * us which column to search. */ GtkComboBox *combo = widgetGetComboBox(dialog); BlxColumnId searchCol = getColumnFromComboBox(combo); GError *error = NULL; GList *seqList = findSeqsFromColumn(blxWindow, inputText, searchCol, FALSE, FALSE, &error); if (error) { reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL); result = FALSE; } if (seqList) { GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); createSequenceGroup(blxWindow, seqList, FALSE, inputText); } return result; } /* Utility function to take a list of search strings (newline separated), and * return a list of BlxSequences whose column data matches one of those search * strings. */ static GList* getSeqStructsFromSearchStringList(GList *searchStringList, GList *seqList, BlxViewContext *bc, const BlxColumnId searchCol, GError **error) { SeqSearchData searchData = {NULL, searchCol, bc, NULL, NULL}; /* Loop through all the names in the input list */ GList *nameItem = searchStringList; for ( ; nameItem; nameItem = nameItem->next) { /* Compare this name to all names in the sequence list. If it matches, * add it to the result list. */ searchData.searchStr = (const char*)(nameItem->data); g_list_foreach(seqList, getSequencesThatMatch, &searchData); if (searchData.error) break; } if (searchData.error) g_propagate_error(error, searchData.error); return searchData.matchList; } /* Utility to create a GList of BlxSequences from a textual list of search strings. * Returns only valid sequences that blixem knows about. Looks for sequences * whose relevent data for the given column matches an item in the input text * (which may be a multi-line list; one search string per line). */ static GList* getSeqStructsFromText(GtkWidget *blxWindow, const char *inputText, const BlxColumnId searchCol, GError **error) { BlxViewContext *bc = blxWindowGetContext(blxWindow); GError *tmpError = NULL; GList *searchStringList = parseMatchList(inputText); /* Extract the entries from the list that are sequences that blixem knows about */ GList *matchSeqs = blxWindowGetAllMatchSeqs(blxWindow); GList *seqList = getSeqStructsFromSearchStringList(searchStringList, matchSeqs, bc, searchCol, &tmpError); if (tmpError) g_propagate_error(error, tmpError); /* Must free the original name list and all its data. */ freeStringList(&searchStringList, TRUE); if (g_list_length(seqList) < 1) { g_list_free(seqList); seqList = NULL; } return seqList; } /* Callback function to be used when requesting text from the clipboard to be used * to create the 'match set' group from the paste text */ static void createMatchSetFromClipboard(GtkClipboard *clipboard, const char *clipboardText, gpointer data) { /* Get the list of sequences to include */ GtkWidget *blxWindow = GTK_WIDGET(data); GList *seqList = getSeqStructsFromText(blxWindow, clipboardText, BLXCOL_SEQNAME, NULL); /* If a group already exists, replace its list. Otherwise create the group. */ if (seqList) { BlxViewContext *blxContext = blxWindowGetContext(blxWindow); if (!blxContext->matchSetGroup) { blxContext->matchSetGroup = createSequenceGroup(blxWindow, seqList, FALSE, MATCH_SET_GROUP_NAME); } else { if (blxContext->matchSetGroup->seqList) { g_list_free(blxContext->matchSetGroup->seqList); } blxContext->matchSetGroup->seqList = seqList; } /* Reset the highlighted/hidden properties to make sure the group is initially visible */ blxContext->matchSetGroup->highlighted = TRUE; blxContext->matchSetGroup->hidden = FALSE; blxWindowGroupsChanged(blxWindow); /* Refresh the groups dialog, if it happens to be open */ refreshDialog(BLXDIALOG_GROUPS, blxWindow); } } /* Callback function to be used when requesting text from the clipboard to be used * to find and select sequences based on the paste text. Scrolls the first selected * match into view. */ void findSeqsFromClipboard(GtkClipboard *clipboard, const char *clipboardText, gpointer data) { /* Get the list of sequences to include */ GtkWidget *blxWindow = GTK_WIDGET(data); GList *seqList = getSeqStructsFromText(blxWindow, clipboardText, BLXCOL_SEQNAME, NULL); if (seqList) { blxWindowSetSelectedSeqList(blxWindow, seqList); firstMatch(blxWindowGetDetailView(blxWindow), seqList, FALSE); } } /* This function toggles the match set. That is, if the match set (a special * group) exists then it deletes it; if it does not exist, then it creates it * from the current clipboard text (which should contain valid sequence name(s)). */ static void toggleMatchSet(GtkWidget *blxWindow) { BlxViewContext *blxContext = blxWindowGetContext(blxWindow); if (blxContext->matchSetGroup && blxContext->matchSetGroup->seqList) { /* Clear the list of names only (don't delete the group, because we want to * keep any changes the user made (e.g. to the group color etc.) for next time. */ g_list_free(blxContext->matchSetGroup->seqList); blxContext->matchSetGroup->seqList = NULL; blxWindowGroupsChanged(blxWindow); /* Refresh the groups dialog, if it happens to be open */ refreshDialog(BLXDIALOG_GROUPS, blxWindow); } else { requestPrimaryClipboardText(createMatchSetFromClipboard, blxWindow); } } /* If the given radio button is enabled, add a group based on the list of sequences * in the given text entry. */ static gboolean onAddGroupFromList(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) { return result; } /* Get the dialog and main window */ GtkWidget *dialog = gtk_widget_get_toplevel(button); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(GTK_WINDOW(dialog))); /* Find the combo box on the dialog (there should only be one), which tells * us which column to search. */ GtkComboBox *combo = widgetGetComboBox(dialog); BlxColumnId searchCol = getColumnFromComboBox(combo); /* The text entry box was passed as the user data. We should have a (multi-line) text view */ const char *inputText = getStringFromTextView(GTK_TEXT_VIEW(data)); GError *error = NULL; GList *seqList = findSeqsFromList(blxWindow, inputText, searchCol, FALSE, FALSE, &error); if (error) { reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL); result = FALSE; } if (seqList) { GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); createSequenceGroup(blxWindow, seqList, FALSE, NULL); } return result; } /* Called when the user has clicked the "delete all groups" button on the "group sequences" dialog. */ static void onButtonClickedDeleteAllGroups(GtkWidget *button, gpointer data) { GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); /* Ask the user if they're sure */ BlxViewContext *bc = blxWindowGetContext(blxWindow); char *title = g_strdup_printf("%sDelete groups", blxGetTitlePrefix(bc)); gint response = runConfirmationBox(blxWindow, title, "This will delete ALL groups. Are you sure?"); g_free(title); if (response == GTK_RESPONSE_ACCEPT) { blxWindowDeleteAllSequenceGroups(blxWindow); /* Close the dialog, because there are no groups left to display. */ gtk_widget_hide_all(GTK_WIDGET(dialogWindow)); } } /* Called when the user has clicked the "delete group" button on the "group sequences" dialog. */ static void onButtonClickedDeleteGroup(GtkWidget *button, gpointer data) { SequenceGroup *group = (SequenceGroup*)data; GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); /* Ask the user if they're sure */ char formatStr[] = "Are you sure you wish to delete group '%s'?"; char messageText[strlen(formatStr) + strlen(group->groupName)]; sprintf(messageText, formatStr, group->groupName); BlxViewContext *bc = blxWindowGetContext(blxWindow); char *title = g_strdup_printf("%sDelete group", blxGetTitlePrefix(bc)); gint response = runConfirmationBox(blxWindow, title, messageText); g_free(title); if (response == GTK_RESPONSE_ACCEPT) { blxWindowDeleteSequenceGroup(blxWindow, group); refreshDialog(BLXDIALOG_GROUPS, blxWindow); } } /* Called when the user chooses a different tabe on the groups dialog */ static gboolean onSwitchPageGroupsDialog(GtkNotebook *notebook, GtkNotebookPage *page, guint pageNum, gpointer data) { GtkDialog *dialog = GTK_DIALOG(data); if (pageNum == 0) { /* For the create-groups page, set the default response to be 'accept' */ gtk_dialog_set_default_response(dialog, GTK_RESPONSE_ACCEPT); } else { /* For other pages, set default response to be 'apply' */ gtk_dialog_set_default_response(dialog, GTK_RESPONSE_APPLY); } return FALSE; } /* Utility to find and return a notebook child of the given widget. Assumes there is only * one - if there are more it will just return the first found. Returns NULL if not found. */ static GtkNotebook* containerGetChildNotebook(GtkContainer *container) { GtkNotebook *result = NULL; GList *children = gtk_container_get_children(container); GList *child = children; for ( ; child; child = child->next) { GtkWidget *childWidget = GTK_WIDGET(child->data); if (GTK_IS_NOTEBOOK(childWidget)) { result = GTK_NOTEBOOK(childWidget); break; } else if (GTK_IS_CONTAINER(childWidget)) { /* recurse */ containerGetChildNotebook(GTK_CONTAINER(childWidget)); } } g_list_free(children); return result; } /* Callback called when user responds to groups dialog */ void onResponseGroupsDialog(GtkDialog *dialog, gint responseId, gpointer data) { gboolean destroy = FALSE; gboolean refresh = FALSE; /* If a notebook was passed, only call callbacks for widgets in the active tab */ GtkNotebook *notebook = containerGetChildNotebook(GTK_CONTAINER(dialog->vbox)); if (!notebook) { g_warning("Expected Groups dialog to contain a notebook widget. Dialog may not refresh properly.\n"); } guint pageNo = notebook ? gtk_notebook_get_current_page(notebook) : 0; GtkWidget *page = notebook ? gtk_notebook_get_nth_page(notebook, pageNo) : NULL; switch (responseId) { case GTK_RESPONSE_ACCEPT: destroy = widgetCallAllCallbacks(page, GINT_TO_POINTER(responseId)); refresh = FALSE; break; case GTK_RESPONSE_APPLY: widgetCallAllCallbacks(page, GINT_TO_POINTER(responseId)); destroy = FALSE; refresh = (pageNo == 0); /* if created a new group, Edit Groups section must be refreshed */ break; case GTK_RESPONSE_CANCEL: case GTK_RESPONSE_REJECT: destroy = TRUE; refresh = FALSE; break; default: break; }; if (destroy) { /* Groups dialog is persistent, so hide it rather than destroying it */ gtk_widget_hide_all(GTK_WIDGET(dialog)); } else if (refresh) { GtkWidget *blxWindow = GTK_WIDGET(data); refreshDialog(BLXDIALOG_GROUPS, blxWindow); } } /* Callback for when the 'hide ungrouped sequences' option is changed */ static gboolean onHideUngroupedChanged(GtkWidget *button, const gint responseId, gpointer data) { setFlagFromButton(button, data); GtkWidget *blxWindow = dialogChildGetBlxWindow(button); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); refilterDetailView(detailView, NULL); calculateExonViewHeight(bigPictureGetFwdExonView(bigPicture)); calculateExonViewHeight(bigPictureGetRevExonView(bigPicture)); forceResize(bigPicture); blxWindowRedrawAll(blxWindow); return TRUE; } /* Create the 'create group' tab of the groups dialog. Appends it to the notebook. */ static void createCreateGroupTab(GtkNotebook *notebook, BlxViewContext *bc, GtkWidget *blxWindow) { const int numRows = 3; const int numCols = 2; const gboolean seqsSelected = g_list_length(bc->selectedSeqs) > 0; /* Put everything in a table */ GtkTable *table = GTK_TABLE(gtk_table_new(numRows, numCols, FALSE)); /* Append the table as a new tab to the notebook */ gtk_notebook_append_page(notebook, GTK_WIDGET(table), gtk_label_new("Create group")); /* Create the left-hand-side column */ GtkRadioButton *button1 = createRadioButton(table, 1, 1, NULL, "_Text search (wildcards * and ?)", !seqsSelected, TRUE, FALSE, onAddGroupFromText, blxWindow, NULL); createRadioButton(table, 1, 2, button1, "_List search", FALSE, TRUE, TRUE, onAddGroupFromList, blxWindow, NULL); createSearchColumnCombo(table, 1, 3, blxWindow); /* Create the right-hand-side column */ createRadioButton(table, 2, 1, button1, "Use current _selection", seqsSelected, FALSE, FALSE, onAddGroupFromSelection, blxWindow, NULL); } /* Create the 'edit groups' tab of the groups dialog. Appends it to the given notebook. */ static void createEditGroupsTab(GtkNotebook *notebook, BlxViewContext *bc, GtkWidget *blxWindow) { const int numRows = g_list_length(bc->sequenceGroups) + 4; /* +4 for: header; delete-all button; hide-all-seqs; hide-all-features */ const int numCols = 6; const int xpad = DEFAULT_TABLE_XPAD; const int ypad = DEFAULT_TABLE_YPAD; int row = 1; /* Put everything in a table */ GtkTable *table = GTK_TABLE(gtk_table_new(numRows, numCols, FALSE)); /* Append the table as a new tab to the notebook */ gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(table), gtk_label_new("Edit groups")); /* Add check buttons to turn on the 'hide ungrouped sequences/features' options */ GtkWidget *hideButton1 = gtk_check_button_new_with_mnemonic("_Hide all sequences not in a group"); GtkWidget *hideButton2 = gtk_check_button_new_with_mnemonic("_Hide all features not in a group"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(hideButton1), bc->flags[BLXFLAG_HIDE_UNGROUPED_SEQS]); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(hideButton2), bc->flags[BLXFLAG_HIDE_UNGROUPED_FEATURES]); widgetSetCallbackData(hideButton1, onHideUngroupedChanged, GINT_TO_POINTER(BLXFLAG_HIDE_UNGROUPED_SEQS)); widgetSetCallbackData(hideButton2, onHideUngroupedChanged, GINT_TO_POINTER(BLXFLAG_HIDE_UNGROUPED_FEATURES)); gtk_table_attach(table, hideButton1, 1, 2, row, row + 1, GTK_FILL, GTK_SHRINK, xpad, ypad); ++row; gtk_table_attach(table, hideButton2, 1, 2, row, row + 1, GTK_FILL, GTK_SHRINK, xpad, ypad); ++row; /* Add labels for each column in the table */ gtk_table_attach(table, gtk_label_new("Group name"), 1, 2, row, row + 1, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, xpad, ypad); gtk_table_attach(table, gtk_label_new("Hide"), 2, 3, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, gtk_label_new("Highlight"), 3, 4, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, gtk_label_new("Order"), 4, 5, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); ++row; /* Add a set of widgets for each group */ GList *groupItem = blxWindowGetSequenceGroups(blxWindow); for ( ; groupItem; groupItem = groupItem->next) { SequenceGroup *group = (SequenceGroup*)(groupItem->data); createEditGroupWidget(blxWindow, group, table, row, xpad, ypad); ++row; } /* Add a button to delete all groups */ GtkWidget *deleteGroupsButton = gtk_button_new_with_label("Delete all groups"); gtk_button_set_image(GTK_BUTTON(deleteGroupsButton), gtk_image_new_from_stock(GTK_STOCK_DELETE, GTK_ICON_SIZE_BUTTON)); gtk_widget_set_size_request(deleteGroupsButton, -1, 30); g_signal_connect(G_OBJECT(deleteGroupsButton), "clicked", G_CALLBACK(onButtonClickedDeleteAllGroups), NULL); gtk_table_attach(table, deleteGroupsButton, numCols - 1, numCols + 1, row, row + 1, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, xpad, ypad); } /* Shows the "Group sequences" dialog. This dialog allows the user to group sequences together. * This tabbed dialog shows both the 'create group' and 'edit groups' dialogs in one. If the * 'editGroups' argument is true and groups exist, the 'Edit Groups' tab is displayed by default; * otherwise the 'Create Groups' tab is shown. */ void showGroupsDialog(GtkWidget *blxWindow, const gboolean editGroups, const gboolean bringToFront) { BlxViewContext *bc = blxWindowGetContext(blxWindow); const BlxDialogId dialogId = BLXDIALOG_GROUPS; GtkWidget *dialog = getPersistentDialog(bc->dialogList, dialogId); if (!dialog) { char *title = g_strdup_printf("%sGroups", blxGetTitlePrefix(bc)); dialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(blxWindow), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, GTK_STOCK_APPLY, GTK_RESPONSE_APPLY, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL); g_free(title); /* These calls are required to make the dialog persistent... */ addPersistentDialog(bc->dialogList, dialogId, dialog); g_signal_connect(dialog, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); /* Make sure we only connect the response event once */ g_signal_connect(dialog, "response", G_CALLBACK(onResponseGroupsDialog), blxWindow); } else { /* Refresh by deleting the dialog contents and re-creating them. */ dialogClearContentArea(GTK_DIALOG(dialog)); } /* Create tabbed pages */ GtkWidget *notebook = gtk_notebook_new(); gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), notebook, TRUE, TRUE, 0); createCreateGroupTab(GTK_NOTEBOOK(notebook), bc, blxWindow); createEditGroupsTab(GTK_NOTEBOOK(notebook), bc, blxWindow); /* Connect signals and show */ gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(blxWindow)); g_signal_connect(notebook, "switch-page", G_CALLBACK(onSwitchPageGroupsDialog), dialog); gtk_widget_show_all(dialog); if (editGroups && notebook && blxWindowGroupsExist(blxWindow)) { gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), 1); /* 'edit' page is the 2nd page */ } else { gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), 0); /* 'create' page is the 1st page */ } if (bringToFront) { /* If user has asked to edit groups (and some groups exist), make the second tab * the default and the 'close' button the default action. (Must do this after showing * the child widgets due to a GTK legacy whereby the notebook won't change tabs otherwise.) */ gtk_window_present(GTK_WINDOW(dialog)); } } /*********************************************************** * Settings menu * ***********************************************************/ /* This function should be called on the child widget of a dialog box that is a transient * child of the main blixem window. It finds the parent dialog of the child and then finds * the blxWindow from the dialog. */ static GtkWidget* dialogChildGetBlxWindow(GtkWidget *child) { GtkWidget *result = NULL; GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(child)); if (dialogWindow) { result = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); } return result; } /* Updates the given flag from the given button. The passed in widget is the toggle button and * the data is an enum indicating which flag was toggled. Returns the new value that was set. */ static gboolean setFlagFromButton(GtkWidget *button, gpointer data) { GtkWidget *blxWindow = dialogChildGetBlxWindow(button); BlxViewContext *bc = blxWindowGetContext(blxWindow); BlxFlag flag = (BlxFlag)GPOINTER_TO_INT(data); const gboolean newValue = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); if (flag > BLXFLAG_MIN && flag < BLXFLAG_NUM_FLAGS) bc->flags[flag] = newValue; return newValue; } /* This callback is called when one of the boolean flags is toggled on the settings dialog. * This generic function sets the flag and redraws everything; if different * updates are required a custom callback function can be used instead. */ static void onToggleFlag(GtkWidget *button, gpointer data) { setFlagFromButton(button, data); GtkWidget *blxWindow = dialogChildGetBlxWindow(button); blxWindowRedrawAll(blxWindow); } /* Callback function called when the 'squash matches' button is toggled */ static void onSquashMatches(GtkWidget *button, gpointer data) { const gboolean squash = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); GtkWidget *blxWindow = dialogChildGetBlxWindow(button); BlxWindowProperties *properties = blxWindowGetProperties(blxWindow); setToggleMenuStatus(properties->actionGroup, "SquashMatches", squash); } /* Callback function called when the 'Show Variation track' button is toggled */ static void onShowVariationTrackToggled(GtkWidget *button, gpointer data) { const gboolean showSnpTrack = setFlagFromButton(button, data); GtkWidget *blxWindow = dialogChildGetBlxWindow(button); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); detailViewUpdateShowSnpTrack(detailView, showSnpTrack); } /* Utility to create a check button with certain given properties, and to pack it into the parent. * Returns the new button. */ static GtkWidget* createCheckButton(GtkBox *box, const char *mnemonic, const gboolean isActive, GCallback callback, gpointer data) { GtkWidget *button = gtk_check_button_new_with_mnemonic(mnemonic); gtk_box_pack_start(box, button, FALSE, FALSE, 0); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), isActive); g_signal_connect(G_OBJECT(button), "toggled", callback, data); return button; } /* Callback to be called when the user has entered a new column size */ static gboolean onColumnSizeChanged(GtkWidget *widget, const gint responseId, gpointer data) { gboolean result = TRUE; GtkEntry *entry = GTK_ENTRY(widget); BlxColumnInfo *columnInfo = (BlxColumnInfo*)data; const gchar *newSizeText = gtk_entry_get_text(entry); const int newWidth = convertStringToInt(newSizeText); if (newWidth != columnInfo->width) { /* Check it's a sensible value. We could do with a better check really but * for now just check that it's less than the screen width. This at least * catches excessively large values, which can cause Blixem to crash. Slightly * too-large values may make things look odd but should be recoverable. */ GtkWidget *blxWindow = dialogChildGetBlxWindow(widget); int maxWidth = 300; gbtools::GUIGetTrueMonitorSize(blxWindow, &maxWidth, NULL); if (newWidth > maxWidth) { g_critical("Column width '%d' too large; not changed.\n", newWidth); } else { columnInfo->width = newWidth; GtkWidget *detailView = blxWindowGetDetailView(blxWindow); updateDynamicColumnWidths(detailView); } } return result; } /* Callback to be called when the user has toggled the visibility of a column */ static gboolean onColumnVisibilityChanged(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; BlxColumnInfo *columnInfo = (BlxColumnInfo*)data; columnInfo->showColumn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); GtkWidget *blxWindow = dialogChildGetBlxWindow(button); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); updateDynamicColumnWidths(detailView); return result; } /* Callback to be called when the user has toggled which columns are included in summary info */ static gboolean onSummaryColumnsChanged(GtkWidget *button, const gint responseId, gpointer data) { gboolean result = TRUE; BlxColumnInfo *columnInfo = (BlxColumnInfo*)data; columnInfo->showSummary = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); GtkWidget *blxWindow = dialogChildGetBlxWindow(button); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); /* Just clear the moused-over feedback area to make sure it's not showing invalid data. The * user can mouse-over again to see the new data. */ clearFeedbackArea(detailView); return result; } /* Just calls gtk_widget_set_sensitive, but so that we can use it as a callback */ static void widgetSetSensitive(GtkWidget *widget, gpointer data) { gboolean sensitive = GPOINTER_TO_INT(data); gtk_widget_set_sensitive(widget, sensitive); } /* Callback when the user hits the 'load optional data' button on the settings dialog */ static void onButtonClickedLoadOptional(GtkWidget *button, gpointer data) { GtkWidget *blxWindow = dialogChildGetBlxWindow(button); BlxViewContext *bc = blxWindowGetContext(blxWindow); /* Create a temporary lookup table for BlxSequences so we can link them on GFF ID */ GHashTable *lookupTable = g_hash_table_new(g_direct_hash, g_direct_equal); GError *error = NULL; gboolean success = bulkFetchSequences( 0, bc->external, bc->flags[BLXFLAG_SAVE_TEMP_FILES], bc->seqType, &bc->matchSeqs, bc->columnList, bc->optionalFetchDefault, bc->fetchMethods, &bc->mspList, &bc->blastMode, bc->featureLists, bc->supportedTypes, NULL, bc->refSeqOffset, &bc->refSeqRange, bc->dataset, TRUE, lookupTable); finaliseFetch(bc->matchSeqs, bc->columnList); if (error) { prefixError(error, "Error loading optional data. "); reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL); } if (success) { /* Set the flag to say that the data has now been loaded */ bc->flags[BLXFLAG_OPTIONAL_COLUMNS] = TRUE; /* Disable the button so user can't try to load data again. */ gtk_widget_set_sensitive(button, FALSE); /* Enable the text entry boxes for all columns. They are all in the container passed * as the user data. */ GtkContainer *container = GTK_CONTAINER(data); gtk_container_foreach(container, widgetSetSensitive, GINT_TO_POINTER(TRUE)); /* Update the flag in all columns to indicate that the data is now loaded */ GtkWidget *detailView = blxWindowGetDetailView(blxWindow); GList *listItem = detailViewGetColumnList(detailView); for ( ; listItem; listItem = listItem->next) { BlxColumnInfo *columnInfo = (BlxColumnInfo*)(listItem->data); columnInfo->dataLoaded = TRUE; } /* Re-sort the trees, because the new data may affect the sort order. Also * resize them, because whether data is loaded affects whether columns are shown. */ detailViewResortTrees(detailView); updateDynamicColumnWidths(detailView); /* Force a of resize the tree columns (updateDynamicColumnWidths won't resize them * because the widths and visibility-flags haven't actually changed, but visibility * IS affected because we've now loaded the data) */ callFuncOnAllDetailViewTrees(detailView, resizeTreeColumns, NULL); resizeDetailViewHeaders(detailView); updateDetailViewRange(detailView); detailViewRedrawAll(detailView); } g_hash_table_unref(lookupTable); } /* Create a button to allow the user to load the data for optional columns, if not already loaded */ static GtkWidget* createColumnLoadDataButton(GtkTable *table, GtkWidget *detailView, int *row, const int cols, int xpad, int ypad) { BlxViewContext *bc = blxWindowGetContext(detailViewGetBlxWindow(detailView)); const gboolean dataLoaded = bc->flags[BLXFLAG_OPTIONAL_COLUMNS]; /* Create the button */ GtkWidget *button = gtk_button_new_with_label(LOAD_DATA_TEXT); gtk_widget_set_sensitive(button, !dataLoaded); /* only enable if data not yet loaded */ /* Add the button to the table, spanning all of the remaining columns */ gtk_table_attach(table, button, 0, 1, *row, *row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); /* Create an explanatory label spanning the rest of the columns */ GtkWidget *label = gtk_label_new("Fetches additional information e.g. from an\nEMBL file (as determined by the config)."); gtk_table_attach(table, label, 1, cols, *row, *row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); *row += 1; return button; } /* Create the settings buttons for a single column */ static void createColumnButton(BlxColumnInfo *columnInfo, GtkTable *table, int *row) { /* Create a label showing the column name */ GtkWidget *label = gtk_label_new(columnInfo->title); gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5); /* Tick-box controlling whether column is displayed */ GtkWidget *showColButton = gtk_check_button_new(); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(showColButton), columnInfo->showColumn); widgetSetCallbackData(showColButton, onColumnVisibilityChanged, (gpointer)columnInfo); /* Tick-box controlling whether column is included in summary info */ GtkWidget *showSummaryButton = gtk_check_button_new(); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(showSummaryButton), columnInfo->showSummary); widgetSetCallbackData(showSummaryButton, onSummaryColumnsChanged, (gpointer)columnInfo); GtkWidget *entry = gtk_entry_new(); if (columnInfo->columnId == BLXCOL_SEQUENCE) { /* The sequence column updates dynamically, so don't allow the user to edit it */ char displayText[] = ""; gtk_entry_set_text(GTK_ENTRY(entry), displayText); gtk_widget_set_sensitive(entry, FALSE); gtk_widget_set_sensitive(showSummaryButton, FALSE); gtk_widget_set_sensitive(showSummaryButton, FALSE); gtk_entry_set_width_chars(GTK_ENTRY(entry), strlen(displayText) + 2); /* fudge up width a bit in case user enters longer text */ } else { /* Grey out all the options if the column hasn't been loaded */ if (!columnInfo->dataLoaded) { gtk_widget_set_sensitive(showColButton, FALSE); gtk_widget_set_sensitive(showSummaryButton, FALSE); gtk_widget_set_sensitive(entry, FALSE); } else if (!columnInfo->canShowSummary) { gtk_widget_set_sensitive(showSummaryButton, FALSE); } char *displayText = convertIntToString(columnInfo->width); gtk_entry_set_text(GTK_ENTRY(entry), displayText); gtk_entry_set_width_chars(GTK_ENTRY(entry), strlen(displayText) + 2); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); widgetSetCallbackData(entry, onColumnSizeChanged, (gpointer)columnInfo); g_free(displayText); } gtk_table_attach(table, label, 0, 1, *row, *row + 1, GTK_FILL, GTK_SHRINK, 4, 4); gtk_table_attach(table, showColButton, 1, 2, *row, *row + 1, GTK_FILL, GTK_SHRINK, 4, 4); gtk_table_attach(table, showSummaryButton, 2, 3, *row, *row + 1, GTK_FILL, GTK_SHRINK, 4, 4); gtk_table_attach(table, entry, 3, 4, *row, *row + 1, GTK_FILL, GTK_SHRINK, 4, 4); *row += 1; } /* Create labels for the column properties widgets created by createColumnButton */ static void createColumnButtonHeaders(GtkTable *table, int *row) { GtkWidget *label = gtk_label_new("Show\ncolumn"); gtk_misc_set_alignment(GTK_MISC(label), 0.0, 1.0); gtk_table_attach(table, label, 1, 2, *row, *row + 1, GTK_FILL, GTK_SHRINK, 4, 4); label = gtk_label_new("Show mouse-\nover details"); gtk_misc_set_alignment(GTK_MISC(label), 0.0, 1.0); gtk_table_attach(table, label, 2, 3, *row, *row + 1, GTK_FILL, GTK_SHRINK, 4, 4); label = gtk_label_new("Column width"); gtk_misc_set_alignment(GTK_MISC(label), 0.0, 1.0); gtk_table_attach(table, label, 3, 4, *row, *row + 1, GTK_FILL, GTK_SHRINK, 4, 4); *row += 1; } /* Create a set of widgets that allow columns settings to be adjusted */ static void createColumnButtons(GtkWidget *parent, GtkWidget *detailView, const int border) { /* put all the column settings in a table. Put the table in a * scrolled window, because there are likely to be many rows */ GtkWidget *scrollWin = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(parent), scrollWin); GList *columnList = detailViewGetColumnList(detailView); const int rows = g_list_length(columnList) + 1; const int cols = 4; int row = 1; GtkTable *table = GTK_TABLE(gtk_table_new(rows, cols, FALSE)); gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrollWin), GTK_WIDGET(table)); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollWin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); /* Create a button to allow the user to load the full EMBL data, if not already loaded */ GtkWidget *button = createColumnLoadDataButton(table, detailView, &row, cols, border, border); /* Loop through each column and create widgets to control the properties */ createColumnButtonHeaders(table, &row); GList *listItem = columnList; for ( ; listItem; listItem = listItem->next) { BlxColumnInfo *columnInfo = (BlxColumnInfo*)(listItem->data); createColumnButton(columnInfo, table, &row); } g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(onButtonClickedLoadOptional), table); } /* Callback to be called when the user has entered a new percent-ID per cell */ static gboolean onIdPerCellChanged(GtkWidget *widget, const gint responseId, gpointer data) { GtkWidget *bigPicture = GTK_WIDGET(data); const char *text = gtk_entry_get_text(GTK_ENTRY(widget)); const gdouble newValue = g_strtod(text, NULL); return bigPictureSetIdPerCell(bigPicture, newValue); } /* Callback to be called when the user has entered a new maximum percent-ID to display */ static gboolean onMaxPercentIdChanged(GtkWidget *widget, const gint responseId, gpointer data) { GtkWidget *bigPicture = GTK_WIDGET(data); const char *text = gtk_entry_get_text(GTK_ENTRY(widget)); const gdouble newValue = g_strtod(text, NULL); return bigPictureSetMaxPercentId(bigPicture, newValue); } /* Callback to be called when the user has entered a new minimum percent-ID to display */ static gboolean onMinPercentIdChanged(GtkWidget *widget, const gint responseId, gpointer data) { GtkWidget *bigPicture = GTK_WIDGET(data); const char *text = gtk_entry_get_text(GTK_ENTRY(widget)); const gdouble newValue = g_strtod(text, NULL); return bigPictureSetMinPercentId(bigPicture, newValue); } /* Callback to be called when the user has changed the depth-per-cell on the coverage view */ static gboolean onDepthPerCellChanged(GtkWidget *widget, const gint responseId, gpointer data) { GtkWidget *coverageView = GTK_WIDGET(data); const char *text = gtk_entry_get_text(GTK_ENTRY(widget)); const gdouble newValue = g_strtod(text, NULL); return coverageViewSetDepthPerCell(coverageView, newValue); } ///* Utility to create a text entry widget displaying the given integer value. The // * given callback will be called when the user OK's the dialog that this widget // * is a child of. */ //static void createTextEntryFromInt(GtkWidget *parent, // const char *title, // const int value, // BlxResponseCallback callbackFunc, // gpointer callbackData) //{ // /* Pack label and text entry into a vbox */ // GtkWidget *vbox = createVBoxWithBorder(parent, 4, FALSE, NULL); // // GtkWidget *label = gtk_label_new(title); // gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); // // GtkWidget *entry = gtk_entry_new(); // gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE, 0); // // char *displayText = convertIntToString(value); // gtk_entry_set_text(GTK_ENTRY(entry), displayText); // // gtk_entry_set_width_chars(GTK_ENTRY(entry), strlen(displayText) + 2); // gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); // // widgetSetCallbackData(entry, callbackFunc, callbackData); // // g_free(displayText); //} /* Utility to create a text entry widget displaying the given double value. The * given callback will be called when the user OK's the dialog that this widget * is a child of. */ static void createTextEntry(GtkWidget *parent, const char *title, const gdouble value, BlxResponseCallback callbackFunc, gpointer callbackData) { /* Pack label and text entry into a vbox */ GtkWidget *vbox = createVBoxWithBorder(parent, 4, FALSE, NULL); GtkWidget *label = gtk_label_new(title); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); GtkWidget *entry = gtk_entry_new(); gtk_box_pack_start(GTK_BOX(vbox), entry, FALSE, FALSE, 0); char *displayText = convertDoubleToString(value, 1); gtk_entry_set_text(GTK_ENTRY(entry), displayText); gtk_entry_set_width_chars(GTK_ENTRY(entry), strlen(displayText) + 2); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); widgetSetCallbackData(entry, callbackFunc, callbackData); g_free(displayText); } /* Create a set of widgets to allow the user to edit grid properties */ static void createGridSettingsButtons(GtkWidget *parent, GtkWidget *bigPicture) { /* Group these buttons in a frame */ GtkWidget *frame = gtk_frame_new("Overview section"); gtk_box_pack_start(GTK_BOX(parent), frame, FALSE, FALSE, 0); /* Arrange the widgets horizontally */ GtkWidget *hbox = createHBoxWithBorder(frame, 12, FALSE, NULL); const DoubleRange* const percentIdRange = bigPictureGetPercentIdRange(bigPicture); createTextEntry(hbox, "%ID per cell", bigPictureGetIdPerCell(bigPicture), onIdPerCellChanged, bigPicture); createTextEntry(hbox, "Max %ID", percentIdRange->max, onMaxPercentIdChanged, bigPicture); createTextEntry(hbox, "Min %ID", percentIdRange->min, onMinPercentIdChanged, bigPicture); } /* Create a set of widgets to allow the user to edit coverage-view properties */ static void createCoverageSettingsButtons(GtkWidget *parent, GtkWidget *bigPicture) { BigPictureProperties *properties = bigPictureGetProperties(bigPicture); /* Group these buttons in a frame */ GtkWidget *frame = gtk_frame_new("Coverage section"); gtk_box_pack_start(GTK_BOX(parent), frame, FALSE, FALSE, 0); /* Arrange the widgets horizontally */ GtkWidget *hbox = createHBoxWithBorder(frame, 12, FALSE, NULL); const double rangePerCell = coverageViewGetDepthPerCell(properties->coverageView); createTextEntry(hbox, "Depth per cell", rangePerCell, onDepthPerCellChanged, properties->coverageView); } /* Callback called when user has changed a blixem color */ static gboolean onChangeBlxColor(GtkWidget *button, const gint responseId, gpointer data) { GdkColor *color = (GdkColor*)data; /* update the color */ gtk_color_button_get_color(GTK_COLOR_BUTTON(button), color); gdk_colormap_alloc_color(gdk_colormap_get_system(), color, TRUE, TRUE); /* Redraw */ GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); blxWindowRedrawAll(blxWindow); return TRUE; } /* Callback called when user has changed the blixem background color */ static gboolean onChangeBackgroundColor(GtkWidget *button, const gint responseId, gpointer data) { GdkColor *color = (GdkColor*)data; /* update the color */ gtk_color_button_get_color(GTK_COLOR_BUTTON(button), color); gdk_colormap_alloc_color(gdk_colormap_get_system(), color, TRUE, TRUE); /* Update */ GtkWindow *dialogWindow = GTK_WINDOW(gtk_widget_get_toplevel(button)); GtkWidget *blxWindow = GTK_WIDGET(gtk_window_get_transient_for(dialogWindow)); onUpdateBackgroundColor(blxWindow); return TRUE; } /* Create a button to allow user to change the color of the given setting */ static void createColorButton(GtkTable *table, GdkColor *color, BlxResponseCallback callbackFunc, gpointer callbackData, const int row, const int column, const int xpad, const int ypad) { GtkWidget *colorButton = gtk_color_button_new_with_color(color); widgetSetCallbackData(colorButton, callbackFunc, callbackData); gtk_table_attach(table, colorButton, column, column + 1, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); } /* Create buttons for the user to be able to change the blixem colour settings */ static void createColorButtons(GtkWidget *parent, GtkWidget *blxWindow, const int borderWidth) { BlxViewContext *bc = blxWindowGetContext(blxWindow); /* put all the colors in a table. Put the table in a scrolled window, because * there are likely to be many rows */ GtkWidget *scrollWin = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(parent), scrollWin); const int numCols = 5; const int numRows = BLXCOL_NUM_COLORS + 1; /* add one for header row */ const int xpad = 2; const int ypad = 2; GtkTable *table = GTK_TABLE(gtk_table_new(numRows, numCols, FALSE)); gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrollWin), GTK_WIDGET(table)); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollWin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); /* Add a header row */ int row = 1; gtk_table_attach(table, gtk_label_new("Normal "), 2, 3, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, gtk_label_new("(selected) "), 3, 4, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, gtk_label_new("Print "), 4, 5, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); gtk_table_attach(table, gtk_label_new("(selected) "), 5, 6, row, row + 1, GTK_SHRINK, GTK_SHRINK, xpad, ypad); /* loop through all defined blixem colours */ int colorId = BLXCOLOR_MIN + 1; for ( ; colorId < BLXCOL_NUM_COLORS; ++colorId) { ++row; BlxColor *blxCol = getBlxColor(bc->defaultColors, colorId); GtkWidget *label = gtk_label_new(blxCol->name); gtk_table_attach(table, label, 1, 2, row, row + 1, (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), GTK_SHRINK, xpad, ypad); /* Special callback for the background color */ BlxResponseCallback callbackFunc = (colorId == BLXCOLOR_BACKGROUND) ? onChangeBackgroundColor : onChangeBlxColor; createColorButton(table, &blxCol->normal, callbackFunc, &blxCol->normal, row, 2, xpad, ypad); createColorButton(table, &blxCol->print, callbackFunc, &blxCol->print, row, 4, xpad, ypad); createColorButton(table, &blxCol->selected, callbackFunc, &blxCol->selected, row, 3, xpad, ypad); createColorButton(table, &blxCol->printSelected, callbackFunc, &blxCol->printSelected, row, 5, xpad, ypad); } } /* Called when the user toggles whether print colors should be used or not */ static void onTogglePrintColors(GtkWidget *button, gpointer data) { GtkWidget *blxWindow = GTK_WIDGET(data); const gboolean usePrintColors = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); blxWindowSetUsePrintColors(blxWindow, usePrintColors); } /* Callback function called when the parent button of a set of sub-buttons is toggled. Enables/disables * the child buttons depending on whether the parent is now active or not. The container widget of the * child buttons is passed as the user data. */ static void onParentBtnToggled(GtkWidget *button, gpointer data) { const gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); GtkWidget *subComponents = GTK_WIDGET(data); gtk_widget_set_sensitive(subComponents, active); } /* Callback function called when the 'Show unaligned bases' or 'Show polyA tails' * buttons are toggled. These are parent buttons so will cause child buttons * to be enabled/disabled. */ static void onShowAdditionalSeqToggled(GtkWidget *button, gpointer data) { onParentBtnToggled(button, data); /* Perform any required updates */ GtkWidget *blxWindow = dialogChildGetBlxWindow(button); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); detailViewUpdateMspLengths(detailView, detailViewGetNumUnalignedBases(detailView)); } /* Callback function called when the 'Limit unaligned bases' button is toggled */ static void onLimitUnalignedBasesToggled(GtkWidget *button, gpointer data) { /* Get the new value */ const gboolean limitUnalignedBases = setFlagFromButton(button, GINT_TO_POINTER(BLXFLAG_LIMIT_UNALIGNED_BASES)); /* Enable/disable the sub-options. Their widgets are all in the container passed as the data. */ GtkWidget *subComponents = GTK_WIDGET(data); gtk_widget_set_sensitive(subComponents, limitUnalignedBases); /* Get the detail view from the main window */ GtkWidget *blxWindow = dialogChildGetBlxWindow(button); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); /* Perform any required updates */ detailViewUpdateMspLengths(detailView, detailViewGetNumUnalignedBases(detailView)); } /* Callback called when the user has changed the number of additional bases to show when the * 'show unaligned bases' option is enabled. */ static gboolean onSetNumUnalignedBases(GtkWidget *entry, const gint responseId, gpointer data) { const char *numStr = gtk_entry_get_text(GTK_ENTRY(entry)); int numBases = convertStringToInt(numStr); GtkWidget *detailView = GTK_WIDGET(data); detailViewSetNumUnalignedBases(detailView, numBases); /* Perform any required updates */ detailViewUpdateMspLengths(detailView, numBases); return TRUE; } /* Callback called when the 'selected seqs only' option of the 'show unaligned bases' * option is toggled. */ static void onToggleShowUnalignedSelected(GtkWidget *button, gpointer data) { /* Get the new value */ setFlagFromButton(button, GINT_TO_POINTER(BLXFLAG_SHOW_UNALIGNED_SELECTED)); /* Perform any required updates */ GtkWidget *detailView = GTK_WIDGET(data); // detailViewUpdateMspLengths(detailView, detailViewGetNumUnalignedBases(detailView)); refilterDetailView(detailView, NULL); } /* Create the check button for the 'limit number of unaligned bases' option on the settings dialog * and pack it into the given container. */ static void createLimitUnalignedBasesButton(GtkContainer *parent, GtkWidget *detailView, BlxViewContext *bc) { /* Create an hbox for the "limit to so-many bases" option, which has a check button, text * entry and some labels. Pack the hbox into the given parent. */ GtkWidget *hbox = gtk_hbox_new(FALSE, 0); gtk_container_add(parent, hbox); /* Create a text entry box so the user can enter the number of bases */ GtkWidget *entry = gtk_entry_new(); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); widgetSetCallbackData(entry, onSetNumUnalignedBases, detailView); DetailViewProperties *properties = detailViewGetProperties(detailView); char *numStr = convertIntToString(properties->numUnalignedBases); gtk_entry_set_text(GTK_ENTRY(entry), numStr); gtk_entry_set_width_chars(GTK_ENTRY(entry), strlen(numStr) + 2); /* fudge up width a bit in case user enters longer text */ g_free(numStr); /* Check button to enable/disable setting the limit */ GtkWidget *button = gtk_check_button_new_with_mnemonic("Li_mit to "); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), bc->flags[BLXFLAG_LIMIT_UNALIGNED_BASES]); g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(onLimitUnalignedBasesToggled), entry); /* Pack it all in the hbox */ gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), entry, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(" additional bases"), FALSE, FALSE, 0); } /* Create a "parent" option button that has a vbox container for "sub-components", i.e. more * option buttons (or other dialog widgets) that will be enabled only when the parent option is * active. Returns the container for the sub-options, which should be packed with the sub-option widgets. */ static GtkContainer* createParentCheckButton(GtkWidget *parent, GtkWidget *detailView, BlxViewContext *bc, const char *label, const BlxFlag flag, GtkWidget **buttonOut, GCallback callbackFunc) { /* We'll the main button and any sub-components into a vbox */ GtkWidget *vbox = gtk_vbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(parent), vbox, FALSE, FALSE, 0); /* Create a vbox for the sub-components. Create the vbox now so we can pass it to the main toggle * button callback, but don't pack it in the container till we've added the main check button. The sub * components are only active if the main check button is active. */ const gboolean active = bc->flags[flag]; GtkWidget *subContainer = gtk_vbox_new(FALSE, 0); gtk_widget_set_sensitive(subContainer, active); /* Main check button to enable/disable the option. This call puts it in the vbox. Set two callbacks: * one to update the flag, and one to enable/disable the child buttons. */ GtkWidget *btn = gtk_check_button_new_with_mnemonic(label); gtk_box_pack_start(GTK_BOX(vbox), btn, FALSE, FALSE, 0); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn), active); /* Connect the toggleFlag callback first so that the flag is set correctly before the callbackFunc is called */ g_signal_connect(G_OBJECT(btn), "toggled", G_CALLBACK(onToggleFlag), GINT_TO_POINTER(flag)); if (callbackFunc) g_signal_connect(G_OBJECT(btn), "toggled", callbackFunc, subContainer); if (buttonOut) *buttonOut = btn; /* Now add the subcomponent container to the vbox. Bit of a hack - put it inside an hbox with * a blank label preceeding it, so that the sub-components appear offset to the right slightly * from the main button. */ GtkWidget *hbox = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(" "), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), subContainer, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); return GTK_CONTAINER(subContainer); } /* Refresh the given dialog, if it is open */ void refreshDialog(const BlxDialogId dialogId, GtkWidget *blxWindow) { /* This is a bit crude but does the job: if the dialog is visible, just call its * 'show' function to re-create its contents. Only need to do anything for persistent * dialogs. Note that we don't want to bring the dialog to the front, just refresh it in * case the user looks at it again. */ if (blxWindow) { BlxViewContext *bc = blxWindowGetContext(blxWindow); GtkWidget *dialog = getPersistentDialog(bc->dialogList, dialogId); if (dialog && GTK_WIDGET_VISIBLE(dialog)) { switch (dialogId) { case BLXDIALOG_SETTINGS: showSettingsDialog(blxWindow, FALSE); break; case BLXDIALOG_SORT: showSortDialog(blxWindow, FALSE); break; case BLXDIALOG_HELP: showHelpDialog(blxWindow, FALSE); break; case BLXDIALOG_FIND: showFindDialog(blxWindow, FALSE); break; case BLXDIALOG_VIEW: showViewPanesDialog(blxWindow, FALSE); break; case BLXDIALOG_DOTTER: showDotterDialog(blxWindow, FALSE); break; case BLXDIALOG_GROUPS: showGroupsDialog(blxWindow, TRUE, FALSE); /* show the 'edit' pane because we've got here by adding/deleting a group */ break; default: break; }; } } else { g_warning("Could not refresh dialog [ID=%d]; parent window not found.\n", dialogId); } } /* Callback called when the user has responded to the font selection dialog */ static void onResponseFontSelectionDialog(GtkDialog *dialog, gint responseId, gpointer data) { if (responseId == GTK_RESPONSE_ACCEPT || responseId == GTK_RESPONSE_OK || responseId == GTK_RESPONSE_APPLY) { GtkWidget *blxWindow = GTK_WIDGET(data); /* Check that the user selected a monospace font (unfortunately there's no easy way to get the * font family in older GTK versions so don't bother checking) */ gboolean ok = TRUE; #if CHECK_GTK_VERSION(2, 6) GtkFontSelection *fontSeln = GTK_FONT_SELECTION(gtk_buildable_get_internal_child(GTK_BUILDABLE(dialog), gtk_builder_new(), "font_selection")); PangoFontFamily *family = gtk_font_selection_get_family(fontSeln); ok = pango_font_family_is_monospace(family); #endif /* Get the selected font name */ gchar *fontName = gtk_font_selection_dialog_get_font_name(GTK_FONT_SELECTION_DIALOG(dialog)); if (!ok) { BlxViewContext *bc = blxWindowGetContext(blxWindow); char *msg = g_strdup_printf("Selected font '%s' is not a fixed-width font. Matches may not appear correctly aligned. Are you sure you want to continue?", fontName); char *title = g_strdup_printf("%sWarning", blxGetTitlePrefix(bc)); gint response = runConfirmationBox(GTK_WIDGET(dialog), "Blixem - Warning", msg); g_free(title); g_free(msg); ok = (response == GTK_RESPONSE_ACCEPT); } if (ok) { GtkWidget *detailView = blxWindowGetDetailView(blxWindow); DetailViewProperties *properties = detailViewGetProperties(detailView); pango_font_description_free(properties->fontDesc); properties->fontDesc = pango_font_description_from_string(fontName); updateDetailViewFontDesc(detailView); g_debug("Set font family to '%s'\n", fontName); blxWindowRedrawAll(blxWindow); if (responseId != GTK_RESPONSE_APPLY) { gtk_widget_destroy(GTK_WIDGET(dialog)); } } } else { /* Cancelled */ gtk_widget_destroy(GTK_WIDGET(dialog)); } } /* Callback for when the font selection button is pressed. Opens the font selection dialog */ static void onFontSelectionButtonPressed(GtkWidget *button, gpointer data) { GtkWidget *dialog = gtk_font_selection_dialog_new("Select fixed-width font"); g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(onResponseFontSelectionDialog), data); gtk_widget_show_all(dialog); } /* Create a button on the settings dialog to open a font-selection dialog */ static void createFontSelectionButton(GtkBox *parent, GtkWidget *blxWindow) { GtkWidget *hbox = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(parent, hbox, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new("Change fixed-width font: "), FALSE, FALSE, 0); GtkWidget *button = gtk_button_new_from_stock(GTK_STOCK_SELECT_FONT); g_signal_connect(G_OBJECT(button), "pressed", G_CALLBACK(onFontSelectionButtonPressed), blxWindow); gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0); } /* This function restores all settings to defaults */ static void resetSettings(GtkWidget *blxWindow) { gint responseId = runConfirmationBox(blxWindow, "Reset all settings", "This will reset all settings to their default values: are you sure you want to continue?"); if (responseId == GTK_RESPONSE_ACCEPT) { resetColumnWidths(blxWindowGetColumnList(blxWindow)); showSettingsDialog(blxWindow, FALSE); } } void onResponseSettingsDialog(GtkDialog *dialog, gint responseId, gpointer data) { if (responseId == BLX_RESPONSE_RESET) resetSettings(dialogChildGetBlxWindow(GTK_WIDGET(dialog))); else onResponseDialog(dialog, responseId, data); /* default handler */ } /* Show/refresh the "Settings" dialog. */ void showSettingsDialog(GtkWidget *blxWindow, const gboolean bringToFront) { BlxViewContext *bc = blxWindowGetContext(blxWindow); const BlxDialogId dialogId = BLXDIALOG_SETTINGS; GtkWidget *dialog = getPersistentDialog(bc->dialogList, dialogId); if (!dialog) { /* note: reset-to-defaults option commented out because it is incomplete: * for now, the help page tells the user to delete the ~/.blixemrc file * to reset to defaults */ char *title = g_strdup_printf("%sSettings", blxGetTitlePrefix(bc)); dialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(blxWindow), GTK_DIALOG_DESTROY_WITH_PARENT, // "Reset to defaults", // BLX_RESPONSE_RESET, GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, GTK_STOCK_APPLY, GTK_RESPONSE_APPLY, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL); g_free(title); int width = 300, height = 200; gbtools::GUIGetTrueMonitorSizeFraction(dialog, 0.33, 0.33, &width, &height); gtk_window_set_default_size(GTK_WINDOW(dialog), width, height); /* These calls are required to make the dialog persistent... */ addPersistentDialog(bc->dialogList, dialogId, dialog); g_signal_connect(dialog, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); g_signal_connect(dialog, "response", G_CALLBACK(onResponseSettingsDialog), GINT_TO_POINTER(TRUE)); } else { /* Need to refresh the dialog contents, so clear and re-create content area */ dialogClearContentArea(GTK_DIALOG(dialog)); } gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_APPLY); int borderWidth = 12; GtkWidget *detailView = blxWindowGetDetailView(blxWindow); GtkWidget *bigPicture = blxWindowGetBigPicture(blxWindow); /* We'll put everything into a tabbed notebook */ GtkWidget *notebook = gtk_notebook_new(); gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), notebook, TRUE, TRUE, 0); /* OPTIONS PAGE */ GtkWidget *optionsPage = gtk_vbox_new(FALSE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(optionsPage), gtk_label_new_with_mnemonic("Opt_ions")); GtkWidget *scrollWin = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(optionsPage), scrollWin); GtkWidget *optionsBox = gtk_vbox_new(FALSE, 0); gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrollWin), GTK_WIDGET(optionsBox)); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollWin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); GtkContainer *variationContainer = createParentCheckButton(optionsBox, detailView, bc, "Highlight _variations in reference sequence", BLXFLAG_HIGHLIGHT_VARIATIONS, NULL, G_CALLBACK(onParentBtnToggled)); createCheckButton(GTK_BOX(variationContainer), "Show variations trac_k", bc->flags[BLXFLAG_SHOW_VARIATION_TRACK], G_CALLBACK(onShowVariationTrackToggled), GINT_TO_POINTER(BLXFLAG_SHOW_VARIATION_TRACK)); /* show-polyA-tails option and its sub-options. Connect onToggleFlag twice to the 'when selected' button to also toggle the 'show signals when selected' button in unison. */ GtkWidget *polyAParentBtn = NULL; GtkContainer *polyAContainer = createParentCheckButton(optionsBox, detailView, bc, "Show polyA _tails", BLXFLAG_SHOW_POLYA_SITE, &polyAParentBtn, G_CALLBACK(onShowAdditionalSeqToggled)); GtkWidget *polyABtn = createCheckButton(GTK_BOX(polyAContainer), "Selected sequences only", bc->flags[BLXFLAG_SHOW_POLYA_SITE_SELECTED], G_CALLBACK(onToggleFlag), GINT_TO_POINTER(BLXFLAG_SHOW_POLYA_SITE_SELECTED)); g_signal_connect(G_OBJECT(polyAParentBtn), "toggled", G_CALLBACK(onToggleFlag), GINT_TO_POINTER(BLXFLAG_SHOW_POLYA_SIG)); g_signal_connect(G_OBJECT(polyABtn), "toggled", G_CALLBACK(onToggleFlag), GINT_TO_POINTER(BLXFLAG_SHOW_POLYA_SIG_SELECTED)); const gboolean squashMatches = (bc->modelId == BLXMODEL_SQUASHED); /* show-unaligned-sequence option and its sub-options */ GtkContainer *unalignContainer = createParentCheckButton(optionsBox, detailView, bc, "Show _unaligned sequence", BLXFLAG_SHOW_UNALIGNED, NULL, G_CALLBACK(onShowAdditionalSeqToggled)); createLimitUnalignedBasesButton(unalignContainer, detailView, bc); createCheckButton(GTK_BOX(unalignContainer), "Selected sequences only", bc->flags[BLXFLAG_SHOW_UNALIGNED_SELECTED], G_CALLBACK(onToggleShowUnalignedSelected), detailView); /* show-colinearity-lines option and its sub-options */ GtkContainer *colinearityContainer = createParentCheckButton(optionsBox, detailView, bc, "Show _colinearity lines", BLXFLAG_SHOW_COLINEARITY, NULL, G_CALLBACK(onParentBtnToggled)); createCheckButton(GTK_BOX(colinearityContainer), "Selected sequences only", bc->flags[BLXFLAG_SHOW_COLINEARITY_SELECTED], G_CALLBACK(onToggleFlag), GINT_TO_POINTER(BLXFLAG_SHOW_COLINEARITY_SELECTED)); createCheckButton(GTK_BOX(optionsBox), "Show Sp_lice Sites for selected seqs", bc->flags[BLXFLAG_SHOW_SPLICE_SITES], G_CALLBACK(onToggleFlag), GINT_TO_POINTER(BLXFLAG_SHOW_SPLICE_SITES)); createCheckButton(GTK_BOX(optionsBox), "_Highlight differences", bc->flags[BLXFLAG_HIGHLIGHT_DIFFS], G_CALLBACK(onToggleFlag), GINT_TO_POINTER(BLXFLAG_HIGHLIGHT_DIFFS)); createCheckButton(GTK_BOX(optionsBox), "_Squash matches", squashMatches, G_CALLBACK(onSquashMatches), NULL); /* DISPLAY PAGE */ GtkWidget *displayPage = gtk_vbox_new(FALSE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(displayPage), gtk_label_new_with_mnemonic("_Display")); GtkWidget *displayScrollWin = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(displayPage), displayScrollWin); GtkWidget *displayBox = gtk_vbox_new(FALSE, 0); gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(displayScrollWin), GTK_WIDGET(displayBox)); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(displayScrollWin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); GtkWidget *settingsBox = createVBoxWithBorder(displayBox, borderWidth, TRUE, "General"); const gboolean usePrintColours = blxWindowGetUsePrintColors(blxWindow); createCheckButton(GTK_BOX(settingsBox), "Use _print colours", usePrintColours, G_CALLBACK(onTogglePrintColors), blxWindow); createFontSelectionButton(GTK_BOX(settingsBox), blxWindow); createGridSettingsButtons(displayBox, bigPicture); createCoverageSettingsButtons(displayBox, bigPicture); /* COLUMNS PAGE */ GtkWidget *columnsPage = gtk_vbox_new(FALSE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(columnsPage), gtk_label_new_with_mnemonic("Colum_ns")); createColumnButtons(columnsPage, detailView, borderWidth); /* COLOURS PAGE */ GtkWidget *appearancePage = gtk_vbox_new(FALSE, borderWidth); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(appearancePage), gtk_label_new_with_mnemonic("Colou_rs")); createColorButtons(appearancePage, blxWindow, borderWidth); gtk_widget_show_all(dialog); if (bringToFront) { gtk_window_present(GTK_WINDOW(dialog)); } } /*********************************************************** * Sort menu * ***********************************************************/ /* See if this widget is a combo box, or if it has a child combo box */ static GtkComboBox* widgetGetComboBox(GtkWidget *widget) { GtkComboBox *result = NULL; if (GTK_IS_COMBO_BOX(widget)) { result = GTK_COMBO_BOX(widget); } else if (GTK_IS_CONTAINER(widget)) { GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); GList *childItem = children; for ( ; childItem; childItem = childItem->next) { GtkWidget *childWidget = GTK_WIDGET(childItem->data); result = widgetGetComboBox(childWidget); if (result) break; } g_list_free(children); } return result; } /* For a drop-down box that contains columns, find which column is currently * selected */ static BlxColumnId getColumnFromComboBox(GtkComboBox *combo) { BlxColumnId result = BLXCOL_NONE; if (combo) { /* Get the combo box value */ GtkTreeIter iter; if (gtk_combo_box_get_active_iter(combo, &iter)) { GtkTreeModel *model = gtk_combo_box_get_model(combo); GValue val = {0}; gtk_tree_model_get_value(model, &iter, SORT_TYPE_COL, &val); result = (BlxColumnId)g_value_get_int(&val); } } return result; } /* Callback function called when the 'invert sort order' button is toggled */ static gboolean onInvertSortChanged(GtkWidget *button, const gint responseId, gpointer data) { const gboolean invert = setFlagFromButton(button, data); GtkWidget *blxWindow = dialogChildGetBlxWindow(button); GtkWidget *detailView = blxWindowGetDetailView(blxWindow); detailViewUpdateSortInverted(detailView, invert); return TRUE; } /* Callback called when the sort order has been changed in the drop-down box */ static gboolean onSortOrderChanged(GtkWidget *widget, const gint responseId, gpointer data) { GtkWidget *detailView = GTK_WIDGET(data); DetailViewProperties *dvProperties = detailViewGetProperties(detailView); GList *columnList = blxWindowGetColumnList(dvProperties->blxWindow); if (GTK_WIDGET_REALIZED(detailView) && GTK_IS_CONTAINER(widget)) { /* Loop through each child of the given widget (assumes that each child is or * contains one combo box) */ GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); GList *childItem = children; const int numColumns = g_list_length(columnList); int priority = 0; for ( ; childItem; childItem = childItem->next, ++priority) { if (priority >= numColumns) { g_critical("Exceeded max number of sort columns (%d).\n", numColumns); break; } /* See if this is a (or has a child) combo box */ GtkWidget *childWidget = GTK_WIDGET(childItem->data); GtkComboBox *combo = widgetGetComboBox(childWidget); if (combo) { dvProperties->sortColumns[priority] = getColumnFromComboBox(combo); } } g_list_free(children); /* Re-sort trees */ detailViewResortTrees(detailView); } return TRUE; } /* Add an option for the sorting drop-down box */ static GtkTreeIter* addSortBoxItem(GtkTreeStore *store, GtkTreeIter *parent, BlxColumnId sortColumn, const char *sortName, BlxColumnId initSortColumn, GtkComboBox *combo) { GtkTreeIter iter; gtk_tree_store_append(store, &iter, parent); gtk_tree_store_set(store, &iter, SORT_TYPE_COL, sortColumn, SORT_TEXT_COL, sortName, -1); if (sortColumn == initSortColumn) { gtk_combo_box_set_active_iter(combo, &iter); } return NULL; } /* Create the combo box used for selecting sort criteria */ static void createSortBox(GtkBox *parent, GtkWidget *detailView, const BlxColumnId initSortColumn, GList *columnList, const char *labelText, const gboolean searchableOnly) { /* Put the label and drop-down in a box */ GtkWidget *box = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(parent, box, FALSE, FALSE, 0); /* Add a label, to make it obvious what the combo box is for */ GtkWidget *label = gtk_label_new(labelText); gtk_label_set_use_markup(GTK_LABEL(label), TRUE); gtk_container_add(GTK_CONTAINER(box), label); /* Create the data for the drop-down box. Use a tree so that we can sort by * multiple criteria. */ GtkTreeStore *store = gtk_tree_store_new(N_SORT_COLUMNS, G_TYPE_INT, G_TYPE_STRING); GtkComboBox *combo = GTK_COMBO_BOX(gtk_combo_box_new_with_model(GTK_TREE_MODEL(store))); g_object_unref(store); gtk_container_add(GTK_CONTAINER(box), GTK_WIDGET(combo)); /* Create a cell renderer to display the sort text. */ GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, FALSE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer, "text", SORT_TEXT_COL, NULL); GtkTreeIter *iter = NULL; /* Add a blank row for the case where nothing is selected (unless we only * want searchable columns, because we can't search the NONE column) */ if (!searchableOnly) iter = addSortBoxItem(store, iter , BLXCOL_NONE, "