diff --git a/blxGff3Parser.c b/blxGff3Parser.c index d89bd5b7c4446d36a29cb7257c95f549be6695bc..b7645e7064237149da5c0de4b6e4eb22cacf26b9 100644 --- a/blxGff3Parser.c +++ b/blxGff3Parser.c @@ -532,6 +532,31 @@ static void parseAttributes(char *attributes, } +/* Utility to remove any GFF3 escape characters from the given string and replace them with + * the real characters they represent. Takes into account special chars for generic attributes, + * and also the Target-name and ID attributes if requested */ +static void replaceGff3EscapeChars(char **text, const gboolean targetNameAtt, const gboolean idAtt) +{ + GString *gstr = g_string_new(*text); + + seqtools_g_string_replace(gstr, "%3b", ";"); + seqtools_g_string_replace(gstr, "%3d", "="); + seqtools_g_string_replace(gstr, "%09", "\t"); + + /* Target-name and ID attributes additionally have whitespace escaped */ + if (targetNameAtt || idAtt) + seqtools_g_string_replace(gstr, "%20", " "); + + /* ID attribute additional has ">" escaped */ + if (idAtt) + seqtools_g_string_replace(gstr, "%e3", ">"); + + g_free(*text); + *text = gstr->str; + g_string_free(gstr, FALSE); +} + + /* Parse a tag/data pair of the format "tag=data" */ static void parseTagDataPair(char *text, const int lineNum, @@ -577,6 +602,7 @@ static void parseTagDataPair(char *text, else if (!strcmp(tokens[0], "url")) { gffData->url = g_strdup(tokens[1]); + replaceGff3EscapeChars(&gffData->url, FALSE, FALSE); } else { diff --git a/detailview.c b/detailview.c index dd0593db3627e64ccda4d2f0c6be3f6b7830f7e0..74347e4fa7ca42bf7b5b7458a2754ea101d467d7 100755 --- a/detailview.c +++ b/detailview.c @@ -1732,15 +1732,16 @@ static void getVariationDisplayRange(const MSP *msp, * when outlining it by the extent of the found variation. Also sets drawBackground to true * if the background of the base should be highlighted in the variation color (i.e. if the base * is part of a selected variation). */ -static gboolean coordAffectedByVariation(const int dnaIdx, - const BlxStrand strand, - const MSP *mspList, - BlxViewContext *bc, - gboolean *drawStartBoundary, - gboolean *drawEndBoundary, - gboolean *drawTopBoundary, - gboolean *drawBottomBoundary, - gboolean *drawBackground) +gboolean coordAffectedByVariation(const int dnaIdx, + const BlxStrand strand, + const MSP *mspList, + BlxViewContext *bc, + const MSP **mspOut, /* the variation we found */ + gboolean *drawStartBoundary, + gboolean *drawEndBoundary, + gboolean *drawTopBoundary, + gboolean *drawBottomBoundary, + gboolean *drawBackground) { gboolean result = FALSE; @@ -1753,6 +1754,9 @@ static gboolean coordAffectedByVariation(const int dnaIdx, { result = TRUE; + if (mspOut) + *mspOut = msp; + /* We draw the start boundary of this base if it's the first base in the variation's range * and the end boundary if it's the last, unless it's a zero-length feature, i.e. an insertion * site; in this case we draw a line to the right of the coord in the direction of the @@ -1760,23 +1764,22 @@ static gboolean coordAffectedByVariation(const int dnaIdx, const gboolean isFirst = (dnaIdx == msp->qRange.min); const gboolean isLast = (dnaIdx == msp->qRange.max); const gboolean isZeroLen = mspIsZeroLenVariation(msp); - - *drawStartBoundary = (isZeroLen ? isLast : isFirst); - *drawEndBoundary = (isZeroLen ? isFirst : isLast); - /* Swap left and right if the display is reversed */ - if (bc->displayRev) - { - gboolean tmp = *drawStartBoundary; - *drawStartBoundary = *drawEndBoundary; - *drawEndBoundary = tmp; - } + if (drawStartBoundary) + *drawStartBoundary = (isZeroLen != bc->displayRev ? isLast : isFirst); + + if (drawEndBoundary) + *drawEndBoundary = (isZeroLen != bc->displayRev ? isFirst : isLast); - *drawTopBoundary = !isZeroLen; - *drawBottomBoundary = !isZeroLen; + if (drawTopBoundary) + *drawTopBoundary = !isZeroLen; + + if (drawBottomBoundary) + *drawBottomBoundary = !isZeroLen; /* Highlight the background if the variation is selected (unless it's a zero-length variation) */ - *drawBackground = !isZeroLen && blxContextIsSeqSelected(bc, msp->sSequence); + if (drawBackground) + *drawBackground = !isZeroLen && blxContextIsSeqSelected(bc, msp->sSequence); break; } @@ -1947,7 +1950,7 @@ void drawHeaderChar(BlxViewContext *bc, { gboolean drawBackground = TRUE; - if (coordAffectedByVariation(dnaIdx, strand, bc->mspList, bc, &drawLeft, &drawRight, &drawTop, &drawBottom, &drawBackground)) + if (coordAffectedByVariation(dnaIdx, strand, bc->mspList, bc, NULL, &drawLeft, &drawRight, &drawTop, &drawBottom, &drawBackground)) { /* The coord is affected by a SNP. Outline it in the "selected" SNP color * (which is darker than the normal color) */ @@ -2914,19 +2917,53 @@ static gboolean onButtonPressSnpTrack(GtkWidget *snpTrack, GdkEventButton *event { case 1: { - /* Select the SNP that was clicked on. */ GtkWidget *detailView = GTK_WIDGET(data); - blxWindowDeselectAllSeqs(detailViewGetBlxWindow(detailView)); + GtkWidget *blxWindow = detailViewGetBlxWindow(detailView); + + if (event->type == GDK_BUTTON_PRESS) /* first click */ + { + /* Select the variation that was clicked on. */ + blxWindowDeselectAllSeqs(blxWindow); - /* The SNP track is not the same width as the sequence column, so pass the - * sequence column header so that we can convert to the correct coords */ - DetailViewColumnInfo *seqColInfo = detailViewGetColumnInfo(detailView, BLXCOL_SEQUENCE); + /* The SNP track is not the same width as the sequence column, so pass the + * sequence column header so that we can convert to the correct coords */ + DetailViewColumnInfo *seqColInfo = detailViewGetColumnInfo(detailView, BLXCOL_SEQUENCE); + + selectClickedSnp(snpTrack, seqColInfo->headerWidget, detailView, event->x, event->y, FALSE, TRUE, UNSET_INT); /* SNPs are always expanded in the SNP track */ + + refreshDetailViewHeaders(detailView); + callFuncOnAllDetailViewTrees(detailView, refreshTreeHeaders, NULL); + } + else if (event->type == GDK_2BUTTON_PRESS) /* double-click */ + { + /* If a variation was double-clicked, open its URL in a browser. If multiple are selected, + * use the last-selected one. */ + GList *seqItem = g_list_last(blxWindowGetSelectedSeqs(blxWindow)); + + if (seqItem) + { + BlxSequence *seq = (BlxSequence*)(seqItem->data); + + /* (The url lives in the MSP, and there is only one MSP in a BlxSequence for a variation, + * so perhaps the url should be moved to the BlxSequence...) */ + if (seq->type == BLXSEQUENCE_VARIATION && g_list_length(seq->mspList) > 0) + { + const MSP const *msp = (const MSP const *)(seq->mspList->data); + + if (msp->url) + { + GError *error = NULL; + seqtoolsLaunchWebBrowser(msp->url, &error); + reportAndClearIfError(&error, G_LOG_LEVEL_CRITICAL); + } + else + { + g_warning("Variation '%s' does not have a URL.\n", mspGetSName(msp)); + } + } + } + } - selectClickedSnp(snpTrack, seqColInfo->headerWidget, detailView, event->x, event->y, FALSE, TRUE, UNSET_INT); /* SNPs are always expanded in the SNP track */ - - refreshDetailViewHeaders(detailView); - callFuncOnAllDetailViewTrees(detailView, refreshTreeHeaders, NULL); - handled = TRUE; break; } diff --git a/detailview.h b/detailview.h index 4c0b7698f990f0235c8a39615ad25e339ac1d533..ed3d841ed3bb215f8e0d708a674a2f92a3bf74e3 100644 --- a/detailview.h +++ b/detailview.h @@ -181,6 +181,17 @@ GHashTable* getRefSeqBasesToHighlight(GtkWidget *detailView, const I void drawColumnSeparatorLine(GtkWidget *widget, GdkDrawable *drawable, GdkGC *gc, const BlxViewContext *bc); gboolean onExposeGenericHeader(GtkWidget *headerWidget, GdkEventExpose *event, gpointer data); +gboolean coordAffectedByVariation(const int dnaIdx, + const BlxStrand strand, + const MSP *mspList, + BlxViewContext *bc, + const MSP **msp, + gboolean *drawStartBoundary, + gboolean *drawEndBoundary, + gboolean *drawTopBoundary, + gboolean *drawBottomBoundary, + gboolean *drawBackground); + void drawHeaderChar(BlxViewContext *bc, DetailViewProperties *properties, const int dnaIdx, diff --git a/detailviewtree.c b/detailviewtree.c index d8f58e3cc9b6e2c4f0ca2567067ca90eb6ea367f..1e7086fc93cf1e20ecbd5b8f3b470d15845b3877 100755 --- a/detailviewtree.c +++ b/detailviewtree.c @@ -1401,6 +1401,7 @@ static gboolean onMouseMoveTree(GtkWidget *tree, GdkEventMotion *event, gpointer !(event->state & GDK_BUTTON4_MASK) && !(event->state & GDK_BUTTON5_MASK)) { + /* Feed back details about the currently-hovered over row in the detail-view statusbar area */ GtkStatusbar *statusBar = treeGetStatusBar(tree); /* Remove any previous message */ @@ -1450,21 +1451,86 @@ static gboolean onMouseMoveTree(GtkWidget *tree, GdkEventMotion *event, gpointer } } - -static gboolean onMouseMoveTreeHeader(GtkWidget *header, GdkEventMotion *event, gpointer data) + +/* Get the display index at the given position in the tree header */ +static int treeHeaderGetCoordAtPos(GtkWidget *header, GtkWidget *tree, const int x, const int y) { - GtkWidget *tree = GTK_WIDGET(data); + int baseIdx = UNSET_INT; - /* The start of the header widget is at the start of the sequence column, so offset the - * coords before propagating to the detail view */ GtkWidget *detailView = treeGetDetailView(tree); - IntRange xRange; - detailViewGetColumnXCoords(detailView, BLXCOL_SEQUENCE, &xRange); - event->x += xRange.min; + GtkAdjustment *adjustment = detailViewGetAdjustment(detailView); - propagateEventMotion(tree, treeGetDetailView(tree), event); + if (x >= 0 && x <= header->allocation.width) + { + /* Get the 0-based char index at x */ + gint charWidth = detailViewGetCharWidth(detailView); + int charIdx = (int)((double)x / (double)charWidth); + + /* Add the start of the scroll range to convert this to the display index */ + baseIdx = charIdx + adjustment->value; + } + else if (x < 0) + { + baseIdx = adjustment->value; + } + else if (x > header->allocation.width) + { + baseIdx = adjustment->value = adjustment->page_size; + } - return FALSE; + return baseIdx; +} + + +static gboolean onMouseMoveTreeHeader(GtkWidget *header, GdkEventMotion *event, gpointer data) +{ + gboolean handled = FALSE; + GtkWidget *tree = GTK_WIDGET(data); + + if (event->state & GDK_BUTTON2_MASK) + { + /* Propagate the event to the detail view. The start of the header widget is at + * the start of the sequence column, so offset the coords before propagating. */ + GtkWidget *detailView = treeGetDetailView(tree); + IntRange xRange; + detailViewGetColumnXCoords(detailView, BLXCOL_SEQUENCE, &xRange); + event->x += xRange.min; + + propagateEventMotion(tree, treeGetDetailView(tree), event); + handled = FALSE; + } + else + { + /* If we're hovering over a base that's affected by a variation, then feed back info + * about the variation to the user. */ + + /* First remove any previous message from the feedback area */ + GtkStatusbar *statusBar = treeGetStatusBar(tree); + guint contextId = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusBar), DETAIL_VIEW_STATUSBAR_CONTEXT); + gtk_statusbar_pop(GTK_STATUSBAR(statusBar), contextId); + + /* Get the index we're hovering over */ + const int displayIdx = treeHeaderGetCoordAtPos(header, tree, event->x, event->y); + GtkWidget *detailView = treeGetDetailView(tree); + BlxViewContext *bc = treeGetContext(tree); + + const int dnaIdx = convertDisplayIdxToDnaIdx(displayIdx, bc->seqType, detailViewGetActiveFrame(detailView), treeGetFrame(tree), bc->numFrames, bc->displayRev, &bc->refSeqRange); + + /* See if there's a variation here */ + const MSP *msp = NULL; + + if (coordAffectedByVariation(dnaIdx, treeGetStrand(tree), bc->mspList, bc, &msp, NULL, NULL, NULL, NULL, NULL)) + { + if (msp && mspGetSName(msp)) + { + char *displayText = mspGetSSeq(msp) ? blxprintf("%s : %s", mspGetSName(msp), mspGetSSeq(msp)) : blxprintf("%s", mspGetSName(msp)); + gtk_statusbar_push(GTK_STATUSBAR(statusBar), contextId, displayText); + g_free(displayText); + } + } + } + + return handled; } @@ -1491,18 +1557,36 @@ static gboolean onEnterTree(GtkWidget *tree, GdkEventCrossing *event, gpointer d } -static gboolean onLeaveTree(GtkWidget *tree, GdkEventCrossing *event, gpointer data) +/* Clear the detail-view feedback area */ +static void clearStatusbar(GtkWidget *tree) { - /* Remove any message that was added by mousing over the tree rows */ GtkStatusbar *statusBar = treeGetStatusBar(tree); guint contextId = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusBar), DETAIL_VIEW_STATUSBAR_CONTEXT); - gtk_statusbar_pop(GTK_STATUSBAR(statusBar), contextId); +} + +static gboolean onLeaveTree(GtkWidget *tree, GdkEventCrossing *event, gpointer data) +{ + /* Remove any statusbar message that was added by mousing over the tree rows */ + clearStatusbar(tree); + /* Return true to stop the default handler re-drawing when the focus changes */ return TRUE; } +static gboolean onLeaveTreeHeader(GtkWidget *header, GdkEventCrossing *event, gpointer data) +{ + /* Remove any statusbar message that was added by mousing over the tree header */ + GtkWidget *tree = GTK_WIDGET(data); + GtkStatusbar *statusBar = treeGetStatusBar(tree); + guint contextId = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusBar), DETAIL_VIEW_STATUSBAR_CONTEXT); + + gtk_statusbar_pop(GTK_STATUSBAR(statusBar), contextId); + + /* Return true to stop the default handler re-drawing when the focus changes */ + return TRUE; +} /* Add a row to the given tree containing the given MSP */ void addMspToTree(GtkWidget *tree, MSP *msp) @@ -2157,14 +2241,16 @@ static TreeColumnHeaderInfo* createTreeColHeader(GList **columnHeaders, gtk_widget_add_events(columnHeader, GDK_BUTTON_PRESS_MASK); gtk_widget_add_events(columnHeader, GDK_BUTTON_RELEASE_MASK); - gtk_widget_add_events(columnHeader, GDK_BUTTON2_MOTION_MASK); + gtk_widget_add_events(columnHeader, GDK_POINTER_MOTION_MASK); + gtk_widget_add_events(columnHeader, GDK_LEAVE_NOTIFY); g_signal_connect(G_OBJECT(columnHeader), "button-press-event", G_CALLBACK(onButtonPressTreeHeader), tree); g_signal_connect(G_OBJECT(columnHeader), "button-release-event", G_CALLBACK(onButtonReleaseTreeHeader), tree); g_signal_connect(G_OBJECT(columnHeader), "motion-notify-event", G_CALLBACK(onMouseMoveTreeHeader), tree); - + g_signal_connect(G_OBJECT(columnHeader), "leave-notify-event", G_CALLBACK(onLeaveTreeHeader), tree); + break; } - + case BLXCOL_START: { /* The start column header displays the start index of the current display range */ @@ -2653,7 +2739,7 @@ GtkWidget* createDetailViewTree(GtkWidget *grid, g_signal_connect(G_OBJECT(tree), "drag-end", G_CALLBACK(onDragEndTree), NULL); g_signal_connect(G_OBJECT(tree), "drag-motion", G_CALLBACK(onDragMotionTree), NULL); g_signal_connect(G_OBJECT(tree), "expose-event", G_CALLBACK(onExposeDetailViewTree), NULL); - + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); g_signal_connect(G_OBJECT(selection), "changed", G_CALLBACK(onSelectionChangedTree), tree); diff --git a/seqtoolsWebBrowser.c b/seqtoolsWebBrowser.c index 3f8d16789b77196ee4acc1b15c96c399308a74f9..8b0034a99c83fa063db1ee15fc7021c732917570 100644 --- a/seqtoolsWebBrowser.c +++ b/seqtoolsWebBrowser.c @@ -49,7 +49,7 @@ typedef struct static char *findBrowser(BrowserConfig browsers, BrowserConfig *browser_out, GError **error) ; static void makeBrowserCmd(GString *cmd, BrowserConfig best_browser, char *url) ; -static char *translateURLChars(char *orig_link) ; +static char *translateURLChars(const char *orig_link) ; gboolean seqtools_g_string_replace(GString *string, char *target, char *source); @@ -119,7 +119,7 @@ static GQuark err_domain_G = 0 ; * @param error pointer to NULL GError pointer for return of errors. * @return gboolean TRUE if launch of browser successful. */ -gboolean seqtoolsLaunchWebBrowser(char *link, GError **error) +gboolean seqtoolsLaunchWebBrowser(const char *link, GError **error) { gboolean result = FALSE ; BrowserConfig best_browser = NULL ; @@ -294,7 +294,7 @@ static void makeBrowserCmd(GString *cmd, BrowserConfig best_browser, char *url) * * mh17: second thoughts: don't use, these rely on quoting: thrid thoughts: no amount of escaped quoting seems to work */ -static char *translateURLChars(char *orig_link) +static char *translateURLChars(const char *orig_link) { char *url = NULL ; diff --git a/utilities.c b/utilities.c index ddc575ea6f36647a38a1f1faba81f830a7366cf9..9e6acaedda03937b79a9aee1769e4c7141b99b00 100644 --- a/utilities.c +++ b/utilities.c @@ -1074,6 +1074,11 @@ int mspGetSEnd(const MSP const *msp) return (mspGetMatchStrand(msp) == mspGetRefStrand(msp) ? msp->sRange.max : msp->sRange.min); } +char* mspGetSSeq(const MSP const *msp) +{ + return (msp && msp->sSequence && msp->sSequence->sequence ? msp->sSequence->sequence->str : NULL); +} + /* Return the match sequence name. (Gets it from the BlxSequence if the MSP itself doesn't have * a name) */ const char *mspGetSName(const MSP const *msp) diff --git a/utilities.h b/utilities.h index 46605ab809d905142a8428122e4c71a19c12f329..712634b1f42939e11b7b2d91f3ac1421bb8d03c9 100644 --- a/utilities.h +++ b/utilities.h @@ -230,6 +230,7 @@ int mspGetQStart(const MSP const *msp); int mspGetQEnd(const MSP const *msp); int mspGetSStart(const MSP const *msp); int mspGetSEnd(const MSP const *msp); +char* mspGetSSeq(const MSP const *msp); int mspGetQRangeLen(const MSP const *msp); int mspGetSRangeLen(const MSP const *msp); int mspGetMatchSeqLen(const MSP const *msp); @@ -394,7 +395,8 @@ void gtk_text_buffer_set_markup (GtkTextBuffer *buffer, /* seqtoolsWebBrowser.c */ -gboolean seqtoolsLaunchWebBrowser(char *link, GError **error); +gboolean seqtoolsLaunchWebBrowser(const char *link, GError **error); +gboolean seqtools_g_string_replace(GString *string, char *target, char *source); #endif /* _utilities_h_included_ */