QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgsdatadefinedbutton.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsdatadefinedbutton.cpp - Data defined selector button
3  --------------------------------------
4  Date : 27-April-2013
5  Copyright : (C) 2013 by Larry Shaffer
6  Email : larrys at dakcarto dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgsdatadefinedbutton.h"
17 
18 #include <qgsapplication.h>
19 #include <qgsdatadefined.h>
21 #include <qgsexpression.h>
22 #include <qgsmessageviewer.h>
23 #include <qgsvectorlayer.h>
24 
25 #include <QClipboard>
26 #include <QMenu>
27 #include <QMouseEvent>
28 #include <QPointer>
29 
30 
31 QIcon QgsDataDefinedButton::mIconDataDefine;
32 QIcon QgsDataDefinedButton::mIconDataDefineOn;
33 QIcon QgsDataDefinedButton::mIconDataDefineError;
34 QIcon QgsDataDefinedButton::mIconDataDefineExpression;
35 QIcon QgsDataDefinedButton::mIconDataDefineExpressionOn;
36 QIcon QgsDataDefinedButton::mIconDataDefineExpressionError;
37 
39  const QgsVectorLayer* vl,
40  const QgsDataDefined* datadefined,
41  DataTypes datatypes,
42  QString description )
43  : QToolButton( parent )
44 {
45  // set up static icons
46  if ( mIconDataDefine.isNull() )
47  {
48  mIconDataDefine = QgsApplication::getThemeIcon( "/mIconDataDefine.svg" );
49  mIconDataDefineOn = QgsApplication::getThemeIcon( "/mIconDataDefineOn.svg" );
50  mIconDataDefineError = QgsApplication::getThemeIcon( "/mIconDataDefineError.svg" );
51  mIconDataDefineExpression = QgsApplication::getThemeIcon( "/mIconDataDefineExpression.svg" );
52  mIconDataDefineExpressionOn = QgsApplication::getThemeIcon( "/mIconDataDefineExpressionOn.svg" );
53  mIconDataDefineExpressionError = QgsApplication::getThemeIcon( "/mIconDataDefineExpressionError.svg" );
54  }
55 
56  setFocusPolicy( Qt::StrongFocus );
57 
58  // set default tool button icon properties
59  setFixedSize( 30, 26 );
60  setStyleSheet( QString( "QToolButton{ background: none; border: 1px solid rgba(0, 0, 0, 0%);} QToolButton:focus { border: 1px solid palette(highlight); }" ) );
61  setIconSize( QSize( 24, 24 ) );
62  setPopupMode( QToolButton::InstantPopup );
63 
64  mDefineMenu = new QMenu( this );
65  connect( mDefineMenu, SIGNAL( aboutToShow() ), this, SLOT( aboutToShowMenu() ) );
66  connect( mDefineMenu, SIGNAL( triggered( QAction* ) ), this, SLOT( menuActionTriggered( QAction* ) ) );
67  setMenu( mDefineMenu );
68 
69  mFieldsMenu = new QMenu( this );
70 
71  mActionDataTypes = new QAction( this );
72  // list fields and types in submenu, since there may be many
73  mActionDataTypes->setMenu( mFieldsMenu );
74 
75  mActionActive = new QAction( this );
76  QFont f = mActionActive->font();
77  f.setBold( true );
78  mActionActive->setFont( f );
79 
80  mActionDescription = new QAction( tr( "Description..." ), this );
81 
82  mActionExpDialog = new QAction( tr( "Edit..." ), this );
83  mActionExpression = 0;
84  mActionPasteExpr = new QAction( tr( "Paste" ), this );
85  mActionCopyExpr = new QAction( tr( "Copy" ), this );
86  mActionClearExpr = new QAction( tr( "Clear" ), this );
87 
88  // set up sibling widget connections
89  connect( this, SIGNAL( dataDefinedActivated( bool ) ), this, SLOT( disableEnabledWidgets( bool ) ) );
90  connect( this, SIGNAL( dataDefinedActivated( bool ) ), this, SLOT( checkCheckedWidgets( bool ) ) );
91 
92  init( vl, datadefined, datatypes, description );
93 }
94 
96 {
97  mEnabledWidgets.clear();
98  mCheckedWidgets.clear();
99 }
100 
102  const QgsDataDefined* datadefined,
103  DataTypes datatypes,
104  QString description )
105 {
106  mVectorLayer = vl;
107  // construct default property if none or incorrect passed in
108  if ( !datadefined )
109  {
110  mProperty.insert( "active", "0" );
111  mProperty.insert( "useexpr", "0" );
112  mProperty.insert( "expression", "" );
113  mProperty.insert( "field", "" );
114  }
115  else
116  {
117  mProperty.insert( "active", datadefined->isActive() ? "1" : "0" );
118  mProperty.insert( "useexpr", datadefined->useExpression() ? "1" : "0" );
119  mProperty.insert( "expression", datadefined->expressionString() );
120  mProperty.insert( "field", datadefined->field() );
121  }
122 
123  mDataTypes = datatypes;
124  mFieldNameList.clear();
125  mFieldTypeList.clear();
126 
127  mInputDescription = description;
128  mFullDescription = QString( "" );
129  mUsageInfo = QString( "" );
130  mCurrentDefinition = QString( "" );
131 
132  // set up data types string
133  mDataTypesString = QString( "" );
134 
135  QStringList ts;
136  if ( mDataTypes.testFlag( String ) )
137  {
138  ts << tr( "string" );
139  }
140  if ( mDataTypes.testFlag( Int ) )
141  {
142  ts << tr( "int" );
143  }
144  if ( mDataTypes.testFlag( Double ) )
145  {
146  ts << tr( "double" );
147  }
148 
149  if ( !ts.isEmpty() )
150  {
151  mDataTypesString = ts.join( ", " );
152  mActionDataTypes->setText( tr( "Field type: " ) + mDataTypesString );
153  }
154 
155  if ( mVectorLayer )
156  {
157  // store just a list of fields of unknown type or those that match the expected type
158  const QgsFields& fields = mVectorLayer->pendingFields();
159  for ( int i = 0; i < fields.count(); ++i )
160  {
161  const QgsField& f = fields.at( i );
162  bool fieldMatch = false;
163  // NOTE: these are the only QVariant enums supported at this time (see QgsField)
164  QString fieldType;
165  switch ( f.type() )
166  {
167  case QVariant::String:
168  fieldMatch = mDataTypes.testFlag( String );
169  fieldType = tr( "string" );
170  break;
171  case QVariant::Int:
172  fieldMatch = mDataTypes.testFlag( Int ) || mDataTypes.testFlag( Double );
173  fieldType = tr( "integer" );
174  break;
175  case QVariant::Double:
176  fieldMatch = mDataTypes.testFlag( Double );
177  fieldType = tr( "double" );
178  break;
179  case QVariant::Invalid:
180  default:
181  fieldMatch = true; // field type is unknown
182  fieldType = tr( "unknown type" );
183  }
184  if ( fieldMatch || mDataTypes.testFlag( AnyType ) )
185  {
186  mFieldNameList << f.name();
187  mFieldTypeList << fieldType;
188  }
189  }
190  }
191 
192  updateGui();
193 }
194 
195 void QgsDataDefinedButton::mouseReleaseEvent( QMouseEvent *event )
196 {
197  // Ctrl-click to toggle activated state
198  if (( event->modifiers() & ( Qt::ControlModifier ) )
199  || event->button() == Qt::RightButton )
200  {
201  setActive( !isActive() );
202  updateGui();
203  event->ignore();
204  return;
205  }
206 
207  // pass to default behaviour
208  QToolButton::mousePressEvent( event );
209 }
210 
211 void QgsDataDefinedButton::aboutToShowMenu()
212 {
213  mDefineMenu->clear();
214 
215  bool hasExp = !getExpression().isEmpty();
216  bool hasField = !getField().isEmpty();
217  QString ddTitle = tr( "Data defined override" );
218 
219  QAction* ddTitleAct = mDefineMenu->addAction( ddTitle );
220  QFont titlefont = ddTitleAct->font();
221  titlefont.setItalic( true );
222  ddTitleAct->setFont( titlefont );
223  ddTitleAct->setEnabled( false );
224 
225  bool addActiveAction = false;
226  if ( useExpression() && hasExp )
227  {
228  QgsExpression exp( getExpression() );
229  // whether expression is parse-able
230  addActiveAction = !exp.hasParserError();
231  }
232  else if ( !useExpression() && hasField )
233  {
234  // whether field exists
235  addActiveAction = mFieldNameList.contains( getField() );
236  }
237 
238  if ( addActiveAction )
239  {
240  ddTitleAct->setText( ddTitle + " (" + ( useExpression() ? tr( "expression" ) : tr( "field" ) ) + ")" );
241  mDefineMenu->addAction( mActionActive );
242  mActionActive->setText( isActive() ? tr( "Deactivate" ) : tr( "Activate" ) );
243  mActionActive->setData( QVariant( isActive() ? false : true ) );
244  }
245 
246  if ( !mFullDescription.isEmpty() )
247  {
248  mDefineMenu->addAction( mActionDescription );
249  }
250 
251  mDefineMenu->addSeparator();
252 
253  if ( !mDataTypesString.isEmpty() )
254  {
255  QAction* fieldTitleAct = mDefineMenu->addAction( tr( "Attribute field" ) );
256  fieldTitleAct->setFont( titlefont );
257  fieldTitleAct->setEnabled( false );
258 
259  mDefineMenu->addAction( mActionDataTypes );
260 
261  mFieldsMenu->clear();
262 
263  if ( mFieldNameList.size() > 0 )
264  {
265 
266  for ( int j = 0; j < mFieldNameList.count(); ++j )
267  {
268  QString fldname = mFieldNameList.at( j );
269  QAction* act = mFieldsMenu->addAction( fldname + " (" + mFieldTypeList.at( j ) + ")" );
270  act->setData( QVariant( fldname ) );
271  if ( getField() == fldname )
272  {
273  act->setCheckable( true );
274  act->setChecked( !useExpression() );
275  }
276  }
277  }
278  else
279  {
280  QAction* act = mFieldsMenu->addAction( tr( "No matching field types found" ) );
281  act->setEnabled( false );
282  }
283 
284  mDefineMenu->addSeparator();
285  }
286 
287  QAction* exprTitleAct = mDefineMenu->addAction( tr( "Expression" ) );
288  exprTitleAct->setFont( titlefont );
289  exprTitleAct->setEnabled( false );
290 
291  if ( hasExp )
292  {
293  QString expString = getExpression();
294  if ( expString.length() > 35 )
295  {
296  expString.truncate( 35 );
297  expString.append( "..." );
298  }
299 
300  expString.prepend( tr( "Current: " ) );
301 
302  if ( !mActionExpression )
303  {
304  mActionExpression = new QAction( expString, this );
305  mActionExpression->setCheckable( true );
306  }
307  else
308  {
309  mActionExpression->setText( expString );
310  }
311  mDefineMenu->addAction( mActionExpression );
312  mActionExpression->setChecked( useExpression() );
313 
314  mDefineMenu->addAction( mActionExpDialog );
315  mDefineMenu->addAction( mActionCopyExpr );
316  mDefineMenu->addAction( mActionPasteExpr );
317  mDefineMenu->addAction( mActionClearExpr );
318  }
319  else
320  {
321  mDefineMenu->addAction( mActionExpDialog );
322  mDefineMenu->addAction( mActionPasteExpr );
323  }
324 
325 }
326 
327 void QgsDataDefinedButton::menuActionTriggered( QAction* action )
328 {
329  if ( action == mActionActive )
330  {
331  setActive( mActionActive->data().toBool() );
332  updateGui();
333  }
334  else if ( action == mActionDescription )
335  {
336  showDescriptionDialog();
337  }
338  else if ( action == mActionExpDialog )
339  {
340  showExpressionDialog();
341  }
342  else if ( action == mActionExpression )
343  {
344  setUseExpression( true );
345  setActive( true );
346  updateGui();
347  }
348  else if ( action == mActionCopyExpr )
349  {
350  QApplication::clipboard()->setText( getExpression() );
351  }
352  else if ( action == mActionPasteExpr )
353  {
354  QString exprString = QApplication::clipboard()->text();
355  if ( !exprString.isEmpty() )
356  {
357  setExpression( exprString );
358  setUseExpression( true );
359  setActive( true );
360  updateGui();
361  }
362  }
363  else if ( action == mActionClearExpr )
364  {
365  // only deactivate if defined expression is being used
366  if ( isActive() && useExpression() )
367  {
368  setUseExpression( false );
369  setActive( false );
370  }
371  setExpression( QString( "" ) );
372  updateGui();
373  }
374  else if ( mFieldsMenu->actions().contains( action ) ) // a field name clicked
375  {
376  if ( action->isEnabled() )
377  {
378  if ( getField() != action->text() )
379  {
380  setField( action->data().toString() );
381  }
382  setUseExpression( false );
383  setActive( true );
384  updateGui();
385  }
386  }
387 }
388 
389 void QgsDataDefinedButton::showDescriptionDialog()
390 {
391  QgsMessageViewer* mv = new QgsMessageViewer( this );
392  mv->setWindowTitle( tr( "Data definition description" ) );
393  mv->setMessageAsHtml( mFullDescription );
394  mv->exec();
395 }
396 
397 void QgsDataDefinedButton::showExpressionDialog()
398 {
399  QgsExpressionBuilderDialog d( const_cast<QgsVectorLayer*>( mVectorLayer ), getExpression() );
400  if ( d.exec() == QDialog::Accepted )
401  {
402  QString newExp = d.expressionText();
403  setExpression( d.expressionText().trimmed() );
404  bool hasExp = !newExp.isEmpty();
405 
406  setUseExpression( hasExp );
407  setActive( hasExp );
408  updateGui();
409  }
410  activateWindow(); // reset focus to parent window
411 }
412 
413 void QgsDataDefinedButton::updateGui()
414 {
415  QString oldDef = mCurrentDefinition;
416  QString newDef( "" );
417  bool hasExp = !getExpression().isEmpty();
418  bool hasField = !getField().isEmpty();
419 
420  if ( useExpression() && !hasExp )
421  {
422  setActive( false );
423  setUseExpression( false );
424  }
425  else if ( !useExpression() && !hasField )
426  {
427  setActive( false );
428  }
429 
430  QIcon icon = mIconDataDefine;
431  QString deftip = tr( "undefined" );
432  if ( useExpression() && hasExp )
433  {
434  icon = isActive() ? mIconDataDefineExpressionOn : mIconDataDefineExpression;
435  newDef = deftip = getExpression();
436 
437  QgsExpression exp( getExpression() );
438  if ( exp.hasParserError() )
439  {
440  setActive( false );
441  icon = mIconDataDefineExpressionError;
442  deftip = tr( "Parse error: %1" ).arg( exp.parserErrorString() );
443  newDef = "";
444  }
445  }
446  else if ( !useExpression() && hasField )
447  {
448  icon = isActive() ? mIconDataDefineOn : mIconDataDefine;
449  newDef = deftip = getField();
450 
451  if ( !mFieldNameList.contains( getField() ) )
452  {
453  setActive( false );
454  icon = mIconDataDefineError;
455  deftip = tr( "'%1' field missing" ).arg( getField() );
456  newDef = "";
457  }
458  }
459 
460  setIcon( icon );
461 
462  // update and emit current definition
463  if ( newDef != oldDef )
464  {
465  mCurrentDefinition = newDef;
466  emit dataDefinedChanged( mCurrentDefinition );
467  }
468 
469  // build full description for tool tip and popup dialog
470  mFullDescription = tr( "<b><u>Data defined override</u></b><br>" );
471 
472  mFullDescription += tr( "<b>Active: </b>%1&nbsp;&nbsp;&nbsp;<i>(ctrl|right-click toggles)</i><br>" ).arg( isActive() ? tr( "yes" ) : tr( "no" ) );
473 
474  if ( !mUsageInfo.isEmpty() )
475  {
476  mFullDescription += tr( "<b>Usage:</b><br>%1<br>" ).arg( mUsageInfo );
477  }
478 
479  if ( !mInputDescription.isEmpty() )
480  {
481  mFullDescription += tr( "<b>Expected input:</b><br>%1<br>" ).arg( mInputDescription );
482  }
483 
484  if ( !mDataTypesString.isEmpty() )
485  {
486  mFullDescription += tr( "<b>Valid input types:</b><br>%1<br>" ).arg( mDataTypesString );
487  }
488 
489  QString deftype( "" );
490  if ( deftip != tr( "undefined" ) )
491  {
492  deftype = QString( " (%1)" ).arg( useExpression() ? tr( "expression" ) : tr( "field" ) );
493  }
494 
495  // truncate long expressions, or tool tip may be too wide for screen
496  if ( deftip.length() > 75 )
497  {
498  deftip.truncate( 75 );
499  deftip.append( "..." );
500  }
501 
502  mFullDescription += tr( "<b>Current definition %1:</b><br>%2" ).arg( deftype ).arg( deftip );
503 
504  setToolTip( mFullDescription );
505 
506 }
507 
509 {
510  if ( isActive() != active )
511  {
512  mProperty.insert( "active", active ? "1" : "0" );
513  emit dataDefinedActivated( active );
514  }
515 }
516 
517 void QgsDataDefinedButton::registerEnabledWidgets( QList<QWidget*> wdgts )
518 {
519  for ( int i = 0; i < wdgts.size(); ++i )
520  {
521  registerEnabledWidget( wdgts.at( i ) );
522  }
523 }
524 
526 {
527  QPointer<QWidget> wdgtP( wdgt );
528  if ( !mEnabledWidgets.contains( wdgtP ) )
529  {
530  mEnabledWidgets.append( wdgtP );
531  }
532 }
533 
535 {
536  QList<QWidget*> wdgtList;
537  for ( int i = 0; i < mEnabledWidgets.size(); ++i )
538  {
539  wdgtList << mEnabledWidgets.at( i );
540  }
541  return wdgtList;
542 }
543 
545 {
546  for ( int i = 0; i < mEnabledWidgets.size(); ++i )
547  {
548  mEnabledWidgets.at( i )->setDisabled( disable );
549  }
550 }
551 
552 void QgsDataDefinedButton::registerCheckedWidgets( QList<QWidget*> wdgts )
553 {
554  for ( int i = 0; i < wdgts.size(); ++i )
555  {
556  registerCheckedWidget( wdgts.at( i ) );
557  }
558 }
559 
561 {
562  QPointer<QWidget> wdgtP( wdgt );
563  if ( !mCheckedWidgets.contains( wdgtP ) )
564  {
565  mCheckedWidgets.append( wdgtP );
566  }
567 }
568 
570 {
571  QList<QWidget*> wdgtList;
572  for ( int i = 0; i < mCheckedWidgets.size(); ++i )
573  {
574  wdgtList << mCheckedWidgets.at( i );
575  }
576  return wdgtList;
577 }
578 
580 {
581  // don't uncheck, only set to checked
582  if ( !check )
583  {
584  return;
585  }
586  for ( int i = 0; i < mCheckedWidgets.size(); ++i )
587  {
588  QAbstractButton *btn = qobject_cast< QAbstractButton * >( mCheckedWidgets.at( i ) );
589  if ( btn && btn->isCheckable() )
590  {
591  btn->setChecked( true );
592  continue;
593  }
594  QGroupBox *grpbx = qobject_cast< QGroupBox * >( mCheckedWidgets.at( i ) );
595  if ( grpbx && grpbx->isCheckable() )
596  {
597  grpbx->setChecked( true );
598  }
599  }
600 }
601 
603 {
604  // just something to reduce translation redundancy
605  return tr( "string " );
606 }
607 
609 {
610  return tr( "bool [<b>1</b>=True|<b>0</b>=False]" );
611 }
612 
614 {
615  return tr( "string of variable length" );
616 }
617 
619 {
620  return tr( "int [&lt;= 0 =&gt;]" );
621 }
622 
624 {
625  return tr( "int [&gt;= 0]" );
626 }
627 
629 {
630  return tr( "int [&gt;= 1]" );
631 }
632 
634 {
635  return tr( "double [&lt;= 0.0 =&gt;]" );
636 }
637 
639 {
640  return tr( "double [&gt;= 0.0]" );
641 }
642 
644 {
645  return tr( "double [0.0-1.0]" );
646 }
647 
649 {
650  return tr( "double coord [<b>X,Y</b>] as &lt;= 0.0 =&gt;" );
651 }
652 
654 {
655  return tr( "double [-180.0 - 180.0]" );
656 }
657 
659 {
660  return tr( "int [0-100]" );
661 }
662 
664 {
665  return trString() + "[<b>MM</b>|<b>MapUnit</b>]";
666 }
667 
669 {
670  return trString() + "[<b>MM</b>|<b>MapUnit</b>|<b>Percent</b>]";
671 }
672 
674 {
675  return tr( "string [<b>r,g,b</b>] as int 0-255" );
676 }
677 
679 {
680  return tr( "string [<b>r,g,b,a</b>] as int 0-255" );
681 }
682 
684 {
685  return trString() + "[<b>Left</b>|<b>Center</b>|<b>Right</b>]";
686 }
687 
689 {
690  return trString() + "[<b>Bottom</b>|<b>Middle</b>|<b>Top</b>]";
691 }
692 
694 {
695  return trString() + "[<b>bevel</b>|<b>miter</b>|<b>round</b>]";
696 }
697 
699 {
700  return trString() + QString( "[<b>Normal</b>|<b>Lighten</b>|<b>Screen</b>|<b>Dodge</b>|<br>"
701  "<b>Addition</b>|<b>Darken</b>|<b>Multiply</b>|<b>Burn</b>|<b>Overlay</b>|<br>"
702  "<b>SoftLight</b>|<b>HardLight</b>|<b>Difference</b>|<b>Subtract</b>]" );
703 }
704 
706 {
707  return trString() + QString( "[<b>filepath</b>] as<br>"
708  "<b>''</b>=empty|absolute|search-paths-relative|<br>"
709  "project-relative|URL" );
710 }
711 
713 {
714  return tr( "string [<b>filepath</b>]" );
715 }
716 
718 {
719  return trString() + QString( "[<b>A5</b>|<b>A4</b>|<b>A3</b>|<b>A2</b>|<b>A1</b>|<b>A0</b>"
720  "<b>B5</b>|<b>B4</b>|<b>B3</b>|<b>B2</b>|<b>B1</b>|<b>B0</b>"
721  "<b>Legal</b>|<b>Ansi A</b>|<b>Ansi B</b>|<b>Ansi C</b>|<b>Ansi D</b>|<b>Ansi E</b>"
722  "<b>Arch A</b>|<b>Arch B</b>|<b>Arch C</b>|<b>Arch D</b>|<b>Arch E</b>|<b>Arch E1</b>]"
723  );
724 }
725 
727 {
728  return trString() + QString( "[<b>portrait</b>|<b>landscape</b>]" );
729 }
730 
732 {
733  return trString() + QString( "[<b>left</b>|<b>center</b>|<b>right</b>]" );
734 }
735 
737 {
738  return trString() + QString( "[<b>top</b>|<b>center</b>|<b>bottom</b>]" );
739 }
740 
742 {
743  return trString() + QString( "[<b>linear</b>|<b>radial</b>|<b>conical</b>]" );
744 }
745 
747 {
748  return trString() + QString( "[<b>feature</b>|<b>viewport</b>]" );
749 }
750 
752 {
753  return trString() + QString( "[<b>pad</b>|<b>repeat</b>|<b>reflect</b>]" );
754 }
755 
757 {
758  return trString() + QString( "[<b>no</b>|<b>solid</b>|<b>dash</b>|<b>dot</b>|<b>dash dot</b>|<b>dash dot dot</b>]" );
759 }
760 
762 {
763  return trString() + QString( "[<b>square</b>|<b>flat</b>|<b>round</b>]" );
764 }
765 
767 {
768  return trString() + QString( "[<b>solid</b>|<b>horizontal</b>|<b>vertical</b>|<b>cross</b>|<b>b_diagonal</b>|<b>f_diagonal"
769  "</b>|<b>diagonal_x</b>|<b>dense1</b>|<b>dense2</b>|<b>dense3</b>|<b>dense4</b>|<b>dense5"
770  "</b>|<b>dense6</b>|<b>dense7</b>|<b>no]" );
771 }
772 
774 {
775  return trString() + QString( "[<b>circle</b>|<b>rectangle</b>|<b>cross</b>|<b>triangle</b>]" );
776 }
777 
779 {
780  return tr( "[<b><dash>;<space></b>] e.g. '8;2;1;2'" );
781 }