21

I am authoring Beamer presentations that feature numerous code listings, and I am required to highlight portions of code with semi-transparent balloons. Yet I've come up with a solution that uses a variant of tikzmark. Inside listings, I am placing anchors manually, stretching a balloon over them afterwards.

\documentclass{beamer}

\usepackage{listings}
\usepackage{tikz}

\usetikzlibrary{fit,calc,shadows}

% Define styles for balloons and lines
\tikzstyle{line} =    [draw, rounded corners=3pt, -latex]
\tikzstyle{balloon} = [draw, fill=blue!20, opacity=0.4, inner sep=4pt, rounded corners=2pt]
\tikzstyle{comment} = [draw, fill=blue!70, text=white, text width=3cm, minimum height=1cm, rounded corners, drop shadow, align=left, font=\scriptsize]

% Command to place a TikZ anchor at the current position
\newcommand{\tikzmark}[1]{%
  \tikz[overlay,remember picture,baseline] \coordinate (#1) at (0,0) {};}

% Command to draw a balloon over two anchors
\newcommand<>{\balloon}[3][balloon]{%
  \coordinate (c) at ($(#2)+(0,1ex)$);
  \node#4 (#1) [balloon, fit=(#3) (c)] {};}

\begin{document}

\begin{frame}[fragile]{Foo!}
\begin{lstlisting}[language=c,escapechar=\$]
$\tikzmark{sw1}$#include <stdio.h>$\tikzmark{ne1}$

int main(void)
{
    printf("hello, world\n");$\tikzmark{ne2}$
    $\tikzmark{sw2}$return 0;
}
\end{lstlisting}

\pause

\begin{tikzpicture}[overlay,remember picture]

  \balloon<2>{ne1}{sw1};
  \balloon<3>{ne2}{sw2};

  \node (comment) [comment] at (\textwidth-1.5cm, .5\textheight+1cm) {%
\only<2>{Include headers}%
\only<3>{Do stuff}%
  };

  \draw<2-3> [line] (comment.south) |- (balloon.east);

\end{tikzpicture}

\end{frame}

\end{document}

Frame 1

Frame 2

As you see, this approach requires a lot of redundant work. I have dozens of presentations to do, many of them containing similar code samples, and placing tikzmarks manually all over the code seems to be an overkill. Additionally, this approach fails if we need to highlight a block of code that contains long lines inside (for example, the whole "main" function in the example above).

In fact, the two line numbers is the only necessary input for this case. It would be good if I could write some macro that would accept two numbers (beginning line and ending line) and the comment text, and do the rest automatically. Could this be done with somehow intercepting lstlisting's output and, for each line, remembering coordinates of the first non-whitespace character and line ending? After that, we would have an array of coordinates that could be, for instance, fed to tikz "fit" facility.

It would be also good if the macro worked not only with inline listings, but with external code too. We are relying on system TTF fonts, that's why we target XeTeX and luaTex (with latter being even preferred). As I'm relatively new to TeX, I would also appreciate any comments and suggestions on the example above.

Thanks!

1
  • 1
    I could think of a way where you need to get listings to insert some code at the start of each line containing something like \tikzmark{<listingID>-<line number>} and then a macro could just use these tikzmarks to draw the highlighting. It has to be listing-width and page-break aware … Oct 30, 2012 at 20:38

1 Answer 1

18

I'll post what I have so far. No doubt it has many flaws that I haven't seen (as well as the ones that I have).

You need the newest version of tikzmark from the TeX-SX Launchpad website. (Make sure you get the most up to date version - I've had reports of issues when downloading but I haven't put in place any sort of revision numbering stuff as yet; the resulting tikzlibrarytikzmark.code.tex will have a command \pgfmark near the end.)

The following puts a modified tikzmark at the start, at the end, and at the first non-whitespace character of each line. It doesn't yet deal with automatic line breaking. The marks are numbered according to the line number, and there's a secret initial start mark at the beginning of the listings environment.

\documentclass{article}

\usepackage{listings}
\usepackage{tikz}
\usetikzlibrary{tikzmark}

\makeatletter
\newif\iflst@linemark

\lst@AddToHook{EveryLine}{%
  \begingroup
  \advance\c@lstnumber by 1\relax
  \pgfmark{line-\lst@name-\the\c@lstnumber-start}%
  \endgroup
}

\lst@AddToHook{EOL}{\pgfmark{line-\lst@name-\the\c@lstnumber-end}%
\global\lst@linemarktrue
}

\lst@AddToHook{OutputBox}{%
  \iflst@linemark
  \pgfmark{line-\lst@name-\the\c@lstnumber-first}%
  \global\lst@linemarkfalse
  \fi
}

\def\tkzlst@fnum#1\relax#2\@STOP{%
  \def\@test{#2}%
  \ifx\@test\@empty
  \def\tkzlst@start{0}%
  \else
  \@tempcnta=#1\relax
  \advance\@tempcnta by -1\relax
  \def\tkzlst@start{\the\@tempcnta}%
  \fi
}

\lst@AddToHook{Init}{%
  \expandafter\tkzlst@fnum\lst@firstnumber\relax\@STOP
  \pgfmark{line-\lst@name-\tkzlst@start-start}%
}

\newcommand\balloon[4]{%
  \pgfmathtruncatemacro\pgf@temp{%
    #3-1
  }%
  \iftikzmark{line-#2-\pgf@temp-start}{%
    \iftikzmark{line-#2-#3-first}{%
      \xdef\b@lines{({pic cs:line-#2-\pgf@temp-start} -| {pic           cs:line-#2-#3-first})}%
    }{%
      \iftikzmark{line-#2-#3-start}{%
        \xdef\b@lines{({pic cs:line-#2-\pgf@temp-start} -| {pic             cs:line-#2-#3-start})}%
      }{%
        \xdef\b@lines{(pic cs:line-#2-\pgf@temp-start)}%
      }%
    }%
  }{%
    \xdef\b@lines{}%
  }%
  \foreach \k in {#3,...,#4} {%
    \iftikzmark{line-#2-\k-first}{%
      \xdef\b@lines{\b@lines (pic cs:line-#2-\k-first) }
    }{}
    \iftikzmark{line-#2-\k-end}{%
      \xdef\b@lines{\b@lines (pic cs:line-#2-\k-end) }
    }{}
  }%
  \ifx\b@lines\pgfutil@empty
  \else
  \edef\pgf@temp{\noexpand\tikz[remember picture,overlay]\noexpand\node[fit={\b@lines},balloon] (#1) {};}%
\pgf@temp
  \fi
}

\makeatother

\usetikzlibrary{fit,calc,shadows}

% Define styles for balloons, lines and marked code areas
\tikzset{
  line/.style={
    draw,
    rounded corners=3pt,
    -latex
  },
  balloon/.style={
    draw,
    fill=blue!20,
    opacity=0.4,
    inner sep=4pt,
    rounded corners=2pt
  },
  comment/.style={
    draw,
    fill=blue!70,
    text=white,
    text width=3cm,
    minimum height=1cm,
    rounded corners,
    drop shadow,
    align=left,
    font=\scriptsize
  },
}

% Command to draw a balloon over two anchors

\begin{document}

\balloon{comment}{code}{1}{1}
\balloon{comment}{code}{3}{3}
\balloon{comment}{more code}{3}{3}
\balloon{comment}{more code}{7}{8}

\begin{tikzpicture}[remember picture,overlay]
\foreach \k in {0,...,7} {
\iftikzmark{line-code-\k-start}{\fill[red] (pic cs:line-code-\k-start) circle[radius=4pt];}{\message{No start for \k}}
\iftikzmark{line-code-\k-end}{\fill[blue] (pic cs:line-code-\k-end) circle[radius=2pt];}{\message{No end for \k}}
\iftikzmark{line-code-\k-first}{\fill[green] (pic cs:line-code-\k-first) circle[radius=2pt];}{\message{No first for \k}}
}
\draw[->] (0,0) -- (pic cs:line-code-5-first);
\draw[->] (0,0) -- (pic cs:line-code-5-start);
\draw[->] (0,0) -- (pic cs:line-code-5-end);
\node[above] at (0,0) {Line 5};
\end{tikzpicture}

Here is some code
\begin{lstlisting}[language=c,name=code,numbers=left]
#include <stdio.h>

int main(void)
{
    printf("hello, world\n");
    return 0;
}
\end{lstlisting}
That's enough of that
\begin{lstlisting}[language=c,name=more code,numbers=left,firstnumber=3]
#include <stdio.h>

int main(void)
{
    printf("hello, world\n");
    return 0;
}
\end{lstlisting}
\end{document}

Result:

marked listings code

7
  • Hi @AndrewStacey: I have problems embedding the modified tikzmark version you mentioned. What do I have to do? Which files do I need to download?
    – Matthias
    Dec 9, 2012 at 22:16
  • @Matthias Download the file tikzmark.dtx and run tex tikzmark.dtx (if you run latex tikzmark.dtx that's fine but you should ignore complaints about a missing file). Put the generated files where tex can find them. I've actually added this code as a tikzmark "extra" so you don't need all the above code (but you can try it as-is). Dec 9, 2012 at 22:18
  • Thanks for the quick reply :) Can I also add them somewhere to a subfolder in my document folder?
    – Matthias
    Dec 9, 2012 at 22:22
  • 2
    Is it possible to reduce the top padding of the balloon? It overlaps (or rather underlaps) the top line, as you can see if you highlight only the return 0; line.
    – Jellby
    Mar 9, 2014 at 18:26
  • 1
    @Jellby AFAICS, it is enough to replace \xdef\b@lines{({pic cs:line-#2-\pgf@temp-start} -| {pic cs:line-#2-#3-first})} by \xdef\b@lines{($ ({pic cs:line-#2-\pgf@temp-start} -| {pic cs:line-#2-#3-first})!.5!({pic cs:line-#2-#3-first}) $)}. No guarantee of no edge effects! :) Jan 31, 2017 at 10:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .