Add missing NetBeans *.form files.
[nbgit.git] / src / org / netbeans / modules / git / ui / log / SearchHistoryPanel.java
blobaed6b530d068c3b3c46a6ff874d0cf41d5c49d5c
1 /*
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6 * The contents of this file are subject to the terms of either the GNU
7 * General Public License Version 2 only ("GPL") or the Common
8 * Development and Distribution License("CDDL") (collectively, the
9 * "License"). You may not use this file except in compliance with the
10 * License. You can obtain a copy of the License at
11 * http://www.netbeans.org/cddl-gplv2.html
12 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13 * specific language governing permissions and limitations under the
14 * License. When distributing the software, include this License Header
15 * Notice in each file and include the License file at
16 * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17 * particular file as subject to the "Classpath" exception as provided
18 * by Sun in the GPL Version 2 section of the License file that
19 * accompanied this code. If applicable, add the following below the
20 * License Header, with the fields enclosed by brackets [] replaced by
21 * your own identifying information:
22 * "Portions Copyrighted [year] [name of copyright owner]"
24 * Contributor(s):
26 * The Original Software is NetBeans. The Initial Developer of the Original
27 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28 * Microsystems, Inc. All Rights Reserved.
29 * Portions Copyright 2008 Alexander Coles (Ikonoklastik Productions).
31 * If you wish your version of this file to be governed by only the CDDL
32 * or only the GPL Version 2, indicate your decision by adding
33 * "[Contributor] elects to include this software in this distribution
34 * under the [CDDL or GPL Version 2] license." If you do not indicate a
35 * single choice of license, a recipient has the option to distribute
36 * your version of this file under either the CDDL, the GPL Version 2 or
37 * to extend the choice of license to its licensees as provided above.
38 * However, if you add GPL Version 2 code and therefore, elected the GPL
39 * Version 2 license, then the option applies only if the new code is
40 * made subject to such option by the copyright holder.
42 package org.netbeans.modules.git.ui.log;
44 import java.awt.Dimension;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ActionListener;
47 import java.awt.event.KeyEvent;
48 import java.beans.PropertyChangeEvent;
49 import java.beans.PropertyChangeListener;
50 import java.io.File;
51 import java.util.ArrayList;
52 import java.util.Collection;
53 import java.util.Collections;
54 import java.util.HashSet;
55 import java.util.List;
56 import java.util.Set;
57 import java.util.StringTokenizer;
58 import javax.swing.AbstractAction;
59 import javax.swing.Action;
60 import javax.swing.KeyStroke;
61 import javax.swing.SwingUtilities;
62 import javax.swing.event.DocumentEvent;
63 import javax.swing.event.DocumentListener;
64 import org.netbeans.modules.git.Git;
65 import org.netbeans.modules.git.GitException;
66 import org.netbeans.modules.git.GitModuleConfig;
67 import org.netbeans.modules.git.ui.diff.DiffSetupSource;
68 import org.netbeans.modules.git.ui.diff.Setup;
69 import org.netbeans.modules.versioning.util.NoContentPanel;
70 import org.openide.awt.Mnemonics;
71 import org.openide.explorer.ExplorerManager;
72 import org.openide.nodes.Node;
73 import org.openide.util.NbBundle;
74 import org.openide.util.RequestProcessor;
75 import org.openide.windows.TopComponent;
77 /**
78 * Contains all components of the Search History panel.
80 * @author Maros Sandor
82 class SearchHistoryPanel extends javax.swing.JPanel implements ExplorerManager.Provider, PropertyChangeListener, ActionListener, DiffSetupSource, DocumentListener {
84 private final File[] roots;
85 private final String repositoryUrl;
86 private final SearchCriteriaPanel criteria;
88 private Divider divider;
89 private Action searchAction;
90 private SearchExecutor currentSearch;
91 private RequestProcessor.Task currentSearchTask;
93 private boolean criteriaVisible;
94 private boolean searchInProgress;
95 private List<RepositoryRevision> results;
96 private SummaryView summaryView;
97 private DiffResultsView diffView;
98 private boolean bOutSearch;
99 private boolean bIncomingSearch;
100 private AbstractAction nextAction;
101 private AbstractAction prevAction;
103 /** Creates new form SearchHistoryPanel */
104 public SearchHistoryPanel(File [] roots, SearchCriteriaPanel criteria) {
105 this.bOutSearch = false;
106 this.bIncomingSearch = false;
107 this.roots = roots;
108 this.repositoryUrl = null;
109 this.criteria = criteria;
110 criteriaVisible = true;
111 explorerManager = new ExplorerManager ();
112 initComponents();
113 setupComponents();
114 refreshComponents(true);
117 public SearchHistoryPanel(String repositoryUrl, File localRoot, SearchCriteriaPanel criteria) {
118 this.bOutSearch = false;
119 this.bIncomingSearch = false;
120 this.repositoryUrl = repositoryUrl;
121 this.roots = new File[] { localRoot };
122 this.criteria = criteria;
123 criteriaVisible = true;
124 explorerManager = new ExplorerManager ();
125 initComponents();
126 setupComponents();
127 refreshComponents(true);
130 void setOutSearch() {
131 criteria.setForOut();
132 bOutSearch = true;
133 divider.setVisible(false);
134 tbSummary.setToolTipText(NbBundle.getMessage(SearchHistoryPanel.class, "TT_OutSummary"));
135 showMergesChkBox.setToolTipText(NbBundle.getMessage(SearchHistoryPanel.class, "TT_OutShowMerges"));
136 tbDiff.setToolTipText(NbBundle.getMessage(SearchHistoryPanel.class, "TT_OutShowDiff"));
139 boolean isOutSearch() {
140 return bOutSearch;
143 boolean isShowMerges() {
144 return showMergesChkBox.isSelected();
148 void setIncomingSearch() {
149 criteria.setForIncoming();
150 bIncomingSearch = true;
151 tbDiff.setVisible(false);
152 bNext.setVisible(false);
153 bPrev.setVisible(false);
154 showMergesChkBox.setToolTipText(NbBundle.getMessage(SearchHistoryPanel.class, "TT_IncomingShowMerges"));
155 tbSummary.setToolTipText(NbBundle.getMessage(SearchHistoryPanel.class, "TT_IncomingSummary"));
158 boolean isIncomingSearch() {
159 return bIncomingSearch;
162 void setSearchCriteria(boolean b) {
163 criteriaVisible = b;
164 refreshComponents(false);
167 private void setupComponents() {
168 remove(jPanel1);
170 divider = new Divider(this);
171 java.awt.GridBagConstraints gridBagConstraints;
172 gridBagConstraints = new java.awt.GridBagConstraints();
173 gridBagConstraints.gridy = 2;
174 gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
175 gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
176 gridBagConstraints.weightx = 1.0;
177 gridBagConstraints.insets = new java.awt.Insets(2, 0, 2, 0);
178 add(divider, gridBagConstraints);
180 searchCriteriaPanel.add(criteria);
181 searchAction = new AbstractAction(NbBundle.getMessage(SearchHistoryPanel.class, "CTL_Search")) { // NOI18N
183 putValue(Action.SHORT_DESCRIPTION, NbBundle.getMessage(SearchHistoryPanel.class, "TT_Search")); // NOI18N
185 public void actionPerformed(ActionEvent e) {
186 search();
189 getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "search"); // NOI18N
190 getActionMap().put("search", searchAction); // NOI18N
191 bSearch.setAction(searchAction);
192 Mnemonics.setLocalizedText(bSearch, NbBundle.getMessage(SearchHistoryPanel.class, "CTL_Search")); // NOI18N
194 Dimension d1 = tbSummary.getPreferredSize();
195 Dimension d2 = tbDiff.getPreferredSize();
196 if (d1.width > d2.width) {
197 tbDiff.setPreferredSize(d1);
200 nextAction = new AbstractAction(null, new javax.swing.ImageIcon(getClass().getResource("/org/netbeans/modules/mercurial/resources/icons/diff-next.png"))) { // NOI18N
202 putValue(Action.SHORT_DESCRIPTION, java.util.ResourceBundle.getBundle("org/netbeans/modules/mercurial/ui/diff/Bundle"). // NOI18N
203 getString("CTL_DiffPanel_Next_Tooltip")); // NOI18N
205 public void actionPerformed(ActionEvent e) {
206 diffView.onNextButton();
209 prevAction = new AbstractAction(null, new javax.swing.ImageIcon(getClass().getResource("/org/netbeans/modules/mercurial/resources/icons/diff-prev.png"))) { // NOI18N
211 putValue(Action.SHORT_DESCRIPTION, java.util.ResourceBundle.getBundle("org/netbeans/modules/mercurial/ui/diff/Bundle"). // NOI18N
212 getString("CTL_DiffPanel_Prev_Tooltip")); // NOI18N
214 public void actionPerformed(ActionEvent e) {
215 diffView.onPrevButton();
218 bNext.setAction(nextAction);
219 bPrev.setAction(prevAction);
221 criteria.tfFrom.getDocument().addDocumentListener(this);
222 criteria.tfTo.getDocument().addDocumentListener(this);
224 getActionMap().put("jumpNext", nextAction); // NOI18N
225 getActionMap().put("jumpPrev", prevAction); // NOI18N
227 showMergesChkBox.setSelected(GitModuleConfig.getDefault().getShowHistoryMerges());
228 showMergesChkBox.setOpaque(false);
231 public void actionPerformed(ActionEvent e) {
232 if (e.getID() == Divider.DIVIDER_CLICKED) {
233 criteriaVisible = !criteriaVisible;
234 refreshComponents(false);
238 private ExplorerManager explorerManager;
240 public void propertyChange(PropertyChangeEvent evt) {
241 if (ExplorerManager.PROP_SELECTED_NODES.equals(evt.getPropertyName())) {
242 TopComponent tc = (TopComponent) SwingUtilities.getAncestorOfClass(TopComponent.class, this);
243 if (tc == null) return;
244 tc.setActivatedNodes((Node[]) evt.getNewValue());
248 public void addNotify() {
249 super.addNotify();
250 explorerManager.addPropertyChangeListener(this);
253 public void removeNotify() {
254 explorerManager.removePropertyChangeListener(this);
255 super.removeNotify();
258 public ExplorerManager getExplorerManager () {
259 return explorerManager;
262 final void refreshComponents(boolean refreshResults) {
263 if (refreshResults) {
264 resultsPanel.removeAll();
265 if (results == null) {
266 if (searchInProgress) {
267 resultsPanel.add(new NoContentPanel(NbBundle.getMessage(SearchHistoryPanel.class, "LBL_SearchHistory_Searching"))); // NOI18N
268 } else {
269 resultsPanel.add(new NoContentPanel(NbBundle.getMessage(SearchHistoryPanel.class, "LBL_SearchHistory_NoResults"))); // NOI18N
271 } else {
272 if (tbSummary.isSelected()) {
273 if (summaryView == null) {
274 summaryView = new SummaryView(this, results);
276 resultsPanel.add(summaryView.getComponent());
277 } else {
278 if (diffView == null) {
279 diffView = new DiffResultsView(this, results);
281 resultsPanel.add(diffView.getComponent());
284 resultsPanel.revalidate();
285 resultsPanel.repaint();
287 nextAction.setEnabled(!tbSummary.isSelected() && diffView != null && diffView.isNextEnabled());
288 prevAction.setEnabled(!tbSummary.isSelected() && diffView != null && diffView.isPrevEnabled());
290 divider.setArrowDirection(criteriaVisible ? Divider.UP : Divider.DOWN);
291 searchCriteriaPanel.setVisible(criteriaVisible);
292 bSearch.setVisible(criteriaVisible);
293 revalidate();
294 repaint();
297 public void setResults(List<RepositoryRevision> newResults) {
298 setResults(newResults, false);
301 private void setResults(List<RepositoryRevision> newResults, boolean searching) {
302 this.results = newResults;
303 this.searchInProgress = searching;
304 summaryView = null;
305 diffView = null;
306 refreshComponents(true);
309 public String getRepositoryUrl() {
310 return repositoryUrl;
313 public String getSearchRepositoryRootUrl() throws GitException {
314 if (repositoryUrl != null) return repositoryUrl;
316 File root = Git.getInstance().getTopmostManagedParent(roots[0]);
317 return root.toString();
320 public File[] getRoots() {
321 return roots;
324 public SearchCriteriaPanel getCriteria() {
325 return criteria;
328 private synchronized void search() {
329 if (currentSearchTask != null) {
330 currentSearchTask.cancel();
332 setResults(null, true);
333 currentSearch = new SearchExecutor(this);
334 currentSearchTask = RequestProcessor.getDefault().create(currentSearch);
335 currentSearchTask.schedule(0);
338 void executeSearch() {
339 search();
342 void showDiff(RepositoryRevision.Event revision) {
343 tbDiff.setSelected(true);
344 refreshComponents(true);
345 diffView.select(revision);
348 public void showDiff(RepositoryRevision container) {
349 tbDiff.setSelected(true);
350 refreshComponents(true);
351 diffView.select(container);
355 * Return diff setup describing shown history.
356 * It return empty collection on non-atomic
357 * revision ranges. XXX move this logic to clients?
359 public Collection getSetups() {
360 if (results == null) {
361 return Collections.EMPTY_SET;
363 if (tbDiff.isSelected()) {
364 return diffView.getSetups();
365 } else {
366 return summaryView.getSetups();
370 Collection getSetups(RepositoryRevision [] revisions, RepositoryRevision.Event [] events) {
371 long fromRevision = Long.MAX_VALUE;
372 long toRevision = Long.MIN_VALUE;
373 Set<File> filesToDiff = new HashSet<File>();
375 for (RepositoryRevision revision : revisions) {
376 long rev = Long.parseLong(revision.getLog().getRevision());
377 if (rev > toRevision) toRevision = rev;
378 if (rev < fromRevision) fromRevision = rev;
379 List<RepositoryRevision.Event> evs = revision.getEvents();
380 for (RepositoryRevision.Event event : evs) {
381 File file = event.getFile();
382 if (file != null) {
383 filesToDiff.add(file);
388 for (RepositoryRevision.Event event : events) {
389 long rev = Long.parseLong(event.getLogInfoHeader().getLog().getRevision());
390 if (rev > toRevision) toRevision = rev;
391 if (rev < fromRevision) fromRevision = rev;
392 if (event.getFile() != null) {
393 filesToDiff.add(event.getFile());
397 List<Setup> setups = new ArrayList<Setup>();
398 for (File file : filesToDiff) {
399 Setup setup = new Setup(file, Long.toString(fromRevision - 1), Long.toString(toRevision));
400 setups.add(setup);
402 return setups;
405 public String getSetupDisplayName() {
406 return null;
409 public static int compareRevisions(String r1, String r2) {
410 StringTokenizer st1 = new StringTokenizer(r1, "."); // NOI18N
411 StringTokenizer st2 = new StringTokenizer(r2, "."); // NOI18N
412 for (;;) {
413 if (!st1.hasMoreTokens()) {
414 return st2.hasMoreTokens() ? -1 : 0;
416 if (!st2.hasMoreTokens()) {
417 return st1.hasMoreTokens() ? 1 : 0;
419 int n1 = Integer.parseInt(st1.nextToken());
420 int n2 = Integer.parseInt(st2.nextToken());
421 if (n1 != n2) return n2 - n1;
425 /** This method is called from within the constructor to
426 * initialize the form.
427 * WARNING: Do NOT modify this code. The content of this method is
428 * always regenerated by the Form Editor.
430 // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
431 private void initComponents() {
432 java.awt.GridBagConstraints gridBagConstraints;
434 buttonGroup1 = new javax.swing.ButtonGroup();
435 searchCriteriaPanel = new javax.swing.JPanel();
436 bSearch = new javax.swing.JButton();
437 jPanel1 = new javax.swing.JPanel();
438 jToolBar1 = new javax.swing.JToolBar();
439 tbSummary = new javax.swing.JToggleButton();
440 tbDiff = new javax.swing.JToggleButton();
441 jSeparator2 = new javax.swing.JSeparator();
442 bNext = new javax.swing.JButton();
443 bPrev = new javax.swing.JButton();
444 jSeparator3 = new javax.swing.JToolBar.Separator();
445 showMergesChkBox = new javax.swing.JCheckBox();
446 resultsPanel = new javax.swing.JPanel();
448 setBorder(javax.swing.BorderFactory.createEmptyBorder(8, 8, 0, 8));
449 setLayout(new java.awt.GridBagLayout());
451 searchCriteriaPanel.setLayout(new java.awt.BorderLayout());
452 gridBagConstraints = new java.awt.GridBagConstraints();
453 gridBagConstraints.gridx = 0;
454 gridBagConstraints.gridy = 0;
455 gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
456 gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
457 gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START;
458 gridBagConstraints.weightx = 1.0;
459 add(searchCriteriaPanel, gridBagConstraints);
461 java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/netbeans/modules/git/ui/log/Bundle"); // NOI18N
462 bSearch.setToolTipText(bundle.getString("TT_Search")); // NOI18N
463 gridBagConstraints = new java.awt.GridBagConstraints();
464 gridBagConstraints.gridx = 0;
465 gridBagConstraints.gridy = 1;
466 gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
467 add(bSearch, gridBagConstraints);
469 jPanel1.setPreferredSize(new java.awt.Dimension(10, 6));
470 gridBagConstraints = new java.awt.GridBagConstraints();
471 gridBagConstraints.gridy = 2;
472 gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
473 gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
474 gridBagConstraints.weightx = 1.0;
475 gridBagConstraints.insets = new java.awt.Insets(0, 0, 2, 0);
476 add(jPanel1, gridBagConstraints);
478 jToolBar1.setFloatable(false);
479 jToolBar1.setRollover(true);
481 buttonGroup1.add(tbSummary);
482 tbSummary.setSelected(true);
483 org.openide.awt.Mnemonics.setLocalizedText(tbSummary, bundle.getString("CTL_ShowSummary")); // NOI18N
484 tbSummary.setToolTipText(bundle.getString("TT_Summary")); // NOI18N
485 tbSummary.addActionListener(new java.awt.event.ActionListener() {
486 public void actionPerformed(java.awt.event.ActionEvent evt) {
487 onViewToggle(evt);
490 jToolBar1.add(tbSummary);
491 tbSummary.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(SearchHistoryPanel.class, "CTL_ShowSummary")); // NOI18N
493 buttonGroup1.add(tbDiff);
494 org.openide.awt.Mnemonics.setLocalizedText(tbDiff, bundle.getString("CTL_ShowDiff")); // NOI18N
495 tbDiff.setToolTipText(bundle.getString("TT_ShowDiff")); // NOI18N
496 tbDiff.addActionListener(new java.awt.event.ActionListener() {
497 public void actionPerformed(java.awt.event.ActionEvent evt) {
498 onViewToggle(evt);
501 jToolBar1.add(tbDiff);
503 jSeparator2.setOrientation(javax.swing.SwingConstants.VERTICAL);
504 jSeparator2.setMaximumSize(new java.awt.Dimension(2, 32767));
505 jToolBar1.add(jSeparator2);
506 jToolBar1.add(bNext);
507 bNext.getAccessibleContext().setAccessibleName("null");
508 bNext.getAccessibleContext().setAccessibleDescription("null");
510 jToolBar1.add(bPrev);
511 bPrev.getAccessibleContext().setAccessibleName("null");
512 bPrev.getAccessibleContext().setAccessibleDescription("null");
514 jToolBar1.add(jSeparator3);
516 showMergesChkBox.setSelected(true);
517 org.openide.awt.Mnemonics.setLocalizedText(showMergesChkBox, org.openide.util.NbBundle.getMessage(SearchHistoryPanel.class, "CTL_ShowMerge")); // NOI18N
518 showMergesChkBox.setToolTipText(org.openide.util.NbBundle.getMessage(SearchHistoryPanel.class, "TT_ShowMerges")); // NOI18N
519 showMergesChkBox.setFocusable(false);
520 showMergesChkBox.setHorizontalTextPosition(javax.swing.SwingConstants.RIGHT);
521 showMergesChkBox.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
522 showMergesChkBox.addChangeListener(new javax.swing.event.ChangeListener() {
523 public void stateChanged(javax.swing.event.ChangeEvent evt) {
524 showMergesChkBoxStateChanged(evt);
527 jToolBar1.add(showMergesChkBox);
529 gridBagConstraints = new java.awt.GridBagConstraints();
530 gridBagConstraints.gridy = 3;
531 gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
532 gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
533 add(jToolBar1, gridBagConstraints);
535 resultsPanel.setLayout(new java.awt.BorderLayout());
536 gridBagConstraints = new java.awt.GridBagConstraints();
537 gridBagConstraints.gridx = 0;
538 gridBagConstraints.gridy = 4;
539 gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
540 gridBagConstraints.gridheight = java.awt.GridBagConstraints.REMAINDER;
541 gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
542 gridBagConstraints.weightx = 1.0;
543 gridBagConstraints.weighty = 1.0;
544 gridBagConstraints.insets = new java.awt.Insets(8, 0, 8, 0);
545 add(resultsPanel, gridBagConstraints);
546 }// </editor-fold>//GEN-END:initComponents
548 private void onViewToggle(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_onViewToggle
549 refreshComponents(true);
550 }//GEN-LAST:event_onViewToggle
552 private void showMergesChkBoxStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_showMergesChkBoxStateChanged
553 GitModuleConfig.getDefault().setShowHistoryMerges( showMergesChkBox.isSelected());
554 }//GEN-LAST:event_showMergesChkBoxStateChanged
556 public void insertUpdate(DocumentEvent e) {
557 validateUserInput();
560 public void removeUpdate(DocumentEvent e) {
561 validateUserInput();
564 public void changedUpdate(DocumentEvent e) {
565 validateUserInput();
568 private void validateUserInput() {
569 String from = criteria.getFrom();
570 if(from == null && criteria.tfFrom.getText().trim().length() > 0) {
571 bSearch.setEnabled(false);
572 return;
574 String to = criteria.getTo();
575 if(to == null && criteria.tfTo.getText().trim().length() > 0) {
576 bSearch.setEnabled(false);
577 return;
579 bSearch.setEnabled(true);
582 // Variables declaration - do not modify//GEN-BEGIN:variables
583 private javax.swing.JButton bNext;
584 private javax.swing.JButton bPrev;
585 private javax.swing.JButton bSearch;
586 private javax.swing.ButtonGroup buttonGroup1;
587 private javax.swing.JPanel jPanel1;
588 private javax.swing.JSeparator jSeparator2;
589 private javax.swing.JToolBar.Separator jSeparator3;
590 private javax.swing.JToolBar jToolBar1;
591 private javax.swing.JPanel resultsPanel;
592 private javax.swing.JPanel searchCriteriaPanel;
593 private javax.swing.JCheckBox showMergesChkBox;
594 private javax.swing.JToggleButton tbDiff;
595 private javax.swing.JToggleButton tbSummary;
596 // End of variables declaration//GEN-END:variables