From 4990d1e7b29d3440523de0c516be279438526a87 Mon Sep 17 00:00:00 2001
From: Jeff Young <jeff@rokeby.ie>
Date: Thu, 26 Mar 2020 11:02:59 +0000
Subject: [PATCH] Variable substitution framework.

This implements editing of variables and moving SCH_FIELDs,
TEXTE_MODULEs, TEXTE_PCB and worksheet items over to the new
framework.
---
 .gitignore                                    |   4 +-
 common/CMakeLists.txt                         |  12 +
 common/common.cpp                             |  66 ++--
 common/dialogs/dialog_configure_paths.cpp     |  60 ++--
 common/dialogs/dialog_page_settings.cpp       |   3 +-
 common/dialogs/panel_text_variables.cpp       | 229 ++++++++++++
 common/dialogs/panel_text_variables_base.cpp  |  98 ++++++
 common/dialogs/panel_text_variables_base.fbp  | 329 ++++++++++++++++++
 common/dialogs/panel_text_variables_base.h    |  57 +++
 common/eda_draw_frame.cpp                     |   5 +-
 common/page_layout/page_layout_reader.cpp     |  77 +++-
 common/page_layout/ws_painter.cpp             | 199 +++++------
 common/page_layout/ws_proxy_view_item.cpp     |  20 +-
 common/plotters/DXF_plotter.cpp               |   2 +-
 common/plotters/common_plot_functions.cpp     |   6 +-
 common/project.cpp                            |  36 +-
 {eeschema => common}/template_fieldnames.cpp  |   0
 {eeschema => common}/template_fieldnames.h    |   0
 .../template_fieldnames.keywords              |   0
 eeschema/CMakeLists.txt                       |   9 -
 eeschema/class_libentry.cpp                   |  11 +-
 eeschema/dialogs/dialog_schematic_setup.cpp   |  31 +-
 eeschema/dialogs/dialog_schematic_setup.h     |   4 +
 eeschema/dialogs/dialog_spice_model.cpp       |  27 +-
 eeschema/dialogs/dialog_update_fields.cpp     |   4 +-
 .../dialogs/panel_eeschema_color_settings.cpp |   2 +-
 eeschema/generate_alias_info.cpp              |   4 +-
 eeschema/lib_field.cpp                        |  33 +-
 eeschema/lib_field.h                          |  21 +-
 .../netlist_exporter_generic.cpp              |   2 +-
 eeschema/plot_schematic_DXF.cpp               |  22 +-
 eeschema/plot_schematic_HPGL.cpp              |  11 +-
 eeschema/plot_schematic_PDF.cpp               |  13 +-
 eeschema/plot_schematic_PS.cpp                |  12 +-
 eeschema/plot_schematic_SVG.cpp               |  12 +-
 eeschema/sch_component.cpp                    |  46 +--
 eeschema/sch_component.h                      |   2 +-
 eeschema/sch_field.cpp                        | 172 +++++++--
 eeschema/sch_field.h                          |  16 +-
 eeschema/sch_item.cpp                         |   4 +-
 eeschema/sch_legacy_plugin.cpp                |   2 +-
 eeschema/sch_painter.cpp                      |   7 +-
 eeschema/sch_sexpr_plugin.cpp                 |   8 +-
 eeschema/sch_sheet_path.cpp                   |  15 +-
 eeschema/sch_view.cpp                         |   1 +
 eeschema/tools/ee_selection_tool.cpp          |  18 +-
 eeschema/tools/lib_edit_tool.cpp              |   2 +-
 eeschema/tools/sch_editor_control.cpp         |   4 +-
 gerbview/gerbview_frame.cpp                   |   4 +-
 include/common.h                              |   9 +
 include/config_params.h                       |   1 +
 include/panel_text_variables.h                |  65 ++++
 include/plotter.h                             |  27 +-
 include/project.h                             |  40 +--
 include/ws_draw_item.h                        |  31 +-
 include/ws_painter.h                          |   2 +-
 include/ws_proxy_view_item.h                  |  74 +---
 pagelayout_editor/pl_draw_panel_gal.cpp       |   2 +
 pcbnew/class_board.cpp                        |   6 +-
 pcbnew/class_board.h                          |  56 +--
 pcbnew/class_module.cpp                       |   4 +-
 pcbnew/class_pcb_text.cpp                     |  23 ++
 pcbnew/class_pcb_text.h                       |   2 +
 pcbnew/class_text_mod.cpp                     |  87 ++---
 pcbnew/dialogs/dialog_board_setup.cpp         |  30 +-
 pcbnew/dialogs/dialog_board_setup.h           |   4 +
 .../dialog_edit_footprint_for_BoardEditor.cpp |  27 +-
 .../dialog_edit_footprint_for_BoardEditor.h   |   2 +
 ...og_edit_footprint_for_BoardEditor_base.cpp |   2 +
 ...og_edit_footprint_for_BoardEditor_base.fbp |   1 +
 ...alog_edit_footprint_for_BoardEditor_base.h |   1 +
 .../dialog_global_edit_text_and_graphics.cpp  |   4 +-
 pcbnew/legacy_plugin.cpp                      |   4 +-
 pcbnew/pcb_edit_frame.cpp                     |   3 +-
 pcbnew/pcb_parser.cpp                         |   6 +-
 pcbnew/plot_board_layers.cpp                  |   4 +-
 pcbnew/plot_brditems_plotter.cpp              |   4 +-
 pcbnew/tools/pcbnew_control.cpp               |   4 +-
 qa/eeschema/lib_field_test_utils.h            |  13 +-
 79 files changed, 1609 insertions(+), 651 deletions(-)
 create mode 100644 common/dialogs/panel_text_variables.cpp
 create mode 100644 common/dialogs/panel_text_variables_base.cpp
 create mode 100644 common/dialogs/panel_text_variables_base.fbp
 create mode 100644 common/dialogs/panel_text_variables_base.h
 rename {eeschema => common}/template_fieldnames.cpp (100%)
 rename {eeschema => common}/template_fieldnames.h (100%)
 rename {eeschema => common}/template_fieldnames.keywords (100%)
 create mode 100644 include/panel_text_variables.h

diff --git a/.gitignore b/.gitignore
index 8cdc745e22..0f69160b27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,8 +17,8 @@ eeschema/cmp_library_lexer.h
 eeschema/cmp_library_keywords.*
 eeschema/dialogs/dialog_bom_cfg_keywords.cpp
 eeschema/dialogs/dialog_bom_cfg_lexer.h
-eeschema/template_fieldnames_keywords.*
-eeschema/template_fieldnames_lexer.h
+common/template_fieldnames_keywords.cpp
+common/template_fieldnames_lexer.h
 eeschema/schematic_keywords.*
 pcbnew/pcb_plot_params_keywords.cpp
 pcbnew/pcb_plot_params_lexer.h
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 675c309ea5..821e787afc 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -184,6 +184,8 @@ set( COMMON_DLG_SRCS
     dialogs/panel_common_settings.cpp
     dialogs/panel_common_settings_base.cpp
     dialogs/panel_hotkeys_editor.cpp
+    dialogs/panel_text_variables.cpp
+    dialogs/panel_text_variables_base.cpp
     dialogs/wx_html_report_panel.cpp
     dialogs/wx_html_report_panel_base.cpp
     )
@@ -334,6 +336,7 @@ set( COMMON_SRCS
     searchhelpfilefullpath.cpp
     status_popup.cpp
     systemdirsappend.cpp
+    template_fieldnames.cpp
     tools_holder.cpp
     trace_helpers.cpp
     undo_redo_container.cpp
@@ -545,6 +548,15 @@ make_lexer(
     LIB_TABLE_T
     )
 
+# auto-generate s-expression template fieldnames lexer and keywords.
+make_lexer(
+    common
+    template_fieldnames.keywords
+    template_fieldnames_lexer.h
+    template_fieldnames_keywords.cpp
+    TFIELD_T
+    )
+
 # auto-generate page layout reader s-expression page_layout_reader_lexer.h
 # and title_block_reader_keywords.cpp.
 make_lexer(
diff --git a/common/common.cpp b/common/common.cpp
index 9eeb1b2cd0..3cc9f2f537 100644
--- a/common/common.cpp
+++ b/common/common.cpp
@@ -25,12 +25,10 @@
 
 #include <fctsys.h>
 #include <eda_base_frame.h>
+#include <project.h>
 #include <common.h>
-#include <macros.h>
 #include <reporter.h>
 #include <mutex>
-#include <settings/settings_manager.h>
-
 #include <wx/process.h>
 #include <wx/config.h>
 #include <wx/utils.h>
@@ -358,23 +356,7 @@ void wxStringSplit( const wxString& aText, wxArrayString& aStrings, wxChar aSpli
 
 int ProcessExecute( const wxString& aCommandLine, int aFlags, wxProcess *callback )
 {
-    return wxExecute( aCommandLine, aFlags, callback );
-}
-
-
-timestamp_t GetNewTimeStamp()
-{
-    static timestamp_t oldTimeStamp;
-    timestamp_t newTimeStamp;
-
-    newTimeStamp = time( NULL );
-
-    if( newTimeStamp <= oldTimeStamp )
-        newTimeStamp = oldTimeStamp + 1;
-
-    oldTimeStamp = newTimeStamp;
-
-    return newTimeStamp;
+    return (int) wxExecute( aCommandLine, aFlags, callback );
 }
 
 
@@ -390,6 +372,50 @@ enum Bracket
 };
 
 
+wxString ExpandTextVars( const wxString& aSource,
+                         const std::function<bool( wxString* )>& aLocalResolver,
+                         const PROJECT* aProject )
+{
+    wxString newbuf;
+    size_t   sourceLen = aSource.length();
+
+    for( size_t i = 0; i < sourceLen; ++i )
+    {
+        if( aSource[i] == '$' && i + 1 < sourceLen && aSource[i+1] == '{' )
+        {
+            wxString token;
+
+            for( i = i + 2; i < sourceLen; ++i )
+            {
+                if( aSource[i] == '}' )
+                    break;
+                else
+                    token.append( aSource[i] );
+            }
+
+            if( token.IsEmpty() )
+                continue;
+
+            if( aLocalResolver( &token ) || ( aProject && aProject->TextVarResolver( &token ) ) )
+            {
+                newbuf.append( token );
+            }
+            else
+            {
+                // Token not resolved: leave the reference unchanged
+                newbuf.append( "${" + token + "}" );
+            }
+        }
+        else
+        {
+            newbuf.append( aSource[i] );
+        }
+    }
+
+    return newbuf;
+}
+
+
 //
 // Stolen from wxExpandEnvVars and then heavily optimized
 //
diff --git a/common/dialogs/dialog_configure_paths.cpp b/common/dialogs/dialog_configure_paths.cpp
index 6fae3cca22..7ace4e6346 100644
--- a/common/dialogs/dialog_configure_paths.cpp
+++ b/common/dialogs/dialog_configure_paths.cpp
@@ -34,11 +34,11 @@
 #include <widgets/wx_grid.h>
 #include <widgets/grid_text_button_helpers.h>
 
-enum ENV_VAR_GRID_COLUMNS
+enum TEXT_VAR_GRID_COLUMNS
 {
-    EV_NAME_COL = 0,
-    EV_PATH_COL,
-    EV_FLAG_COL
+    TV_NAME_COL = 0,
+    TV_VALUE_COL,
+    TV_FLAG_COL
 };
 
 enum SEARCH_PATH_GRID_COLUMNS
@@ -64,16 +64,16 @@ DIALOG_CONFIGURE_PATHS::DIALOG_CONFIGURE_PATHS( wxWindow* aParent, FILENAME_RESO
 
     m_EnvVars->DeleteRows( 0, m_EnvVars->GetNumberRows() );
     m_EnvVars->AppendCols( 1 );     // for the isExternal flags
-    m_EnvVars->HideCol( EV_FLAG_COL );
+    m_EnvVars->HideCol( TV_FLAG_COL );
     m_EnvVars->UseNativeColHeader( true );
 
     wxGridCellAttr* attr = new wxGridCellAttr;
     attr->SetEditor( new GRID_CELL_PATH_EDITOR( this, &m_curdir, wxEmptyString ) );
-    m_EnvVars->SetColAttr( EV_PATH_COL, attr );
+    m_EnvVars->SetColAttr( TV_VALUE_COL, attr );
 
     attr = new wxGridCellAttr;
     attr->SetEditor( new GRID_CELL_PATH_EDITOR( this, &m_curdir, wxEmptyString ) );
-    m_SearchPaths->SetColAttr( EV_PATH_COL, attr );
+    m_SearchPaths->SetColAttr( TV_VALUE_COL, attr );
 
     // Give a bit more room for combobox editors
     m_EnvVars->SetDefaultRowSize( m_EnvVars->GetDefaultRowSize() + 4 );
@@ -167,23 +167,23 @@ void DIALOG_CONFIGURE_PATHS::AppendEnvVar( const wxString& aName, const wxString
 
     m_EnvVars->AppendRows( 1 );
 
-    m_EnvVars->SetCellValue( i, EV_NAME_COL, aName );
+    m_EnvVars->SetCellValue( i, TV_NAME_COL, aName );
 
-    wxGridCellAttr* nameCellAttr = m_EnvVars->GetOrCreateCellAttr( i, EV_NAME_COL );
+    wxGridCellAttr* nameCellAttr = m_EnvVars->GetOrCreateCellAttr( i, TV_NAME_COL );
     wxGridCellTextEditor* nameTextEditor = new GRID_CELL_TEXT_EDITOR();
     nameTextEditor->SetValidator( ENV_VAR_NAME_VALIDATOR() );
     nameCellAttr->SetEditor( nameTextEditor );
     nameCellAttr->SetReadOnly( IsEnvVarImmutable( aName ) );
     nameCellAttr->DecRef();
 
-    m_EnvVars->SetCellValue( i, EV_PATH_COL, aPath );
+    m_EnvVars->SetCellValue( i, TV_VALUE_COL, aPath );
 
-    wxGridCellAttr* pathCellAttr = m_EnvVars->GetOrCreateCellAttr( i, EV_PATH_COL );
+    wxGridCellAttr* pathCellAttr = m_EnvVars->GetOrCreateCellAttr( i, TV_VALUE_COL );
     wxSystemColour c = isExternal ? wxSYS_COLOUR_MENU : wxSYS_COLOUR_LISTBOX;
     pathCellAttr->SetBackgroundColour( wxSystemSettings::GetColour( c ) );
     pathCellAttr->DecRef();
 
-    m_EnvVars->SetCellValue( i, EV_FLAG_COL, isExternal ? wxT( "external" ) : wxEmptyString );
+    m_EnvVars->SetCellValue( i, TV_FLAG_COL, isExternal ? wxT( "external" ) : wxEmptyString );
 }
 
 
@@ -221,9 +221,9 @@ bool DIALOG_CONFIGURE_PATHS::TransferDataFromWindow()
 
     for( int row = 0; row < m_EnvVars->GetNumberRows(); ++row )
     {
-        wxString name = m_EnvVars->GetCellValue( row, EV_NAME_COL );
-        wxString path = m_EnvVars->GetCellValue( row, EV_PATH_COL );
-        wxString external = m_EnvVars->GetCellValue( row, EV_FLAG_COL );
+        wxString name = m_EnvVars->GetCellValue( row, TV_NAME_COL );
+        wxString path = m_EnvVars->GetCellValue( row, TV_VALUE_COL );
+        wxString external = m_EnvVars->GetCellValue( row, TV_FLAG_COL );
 
         if( external.Length() )
             continue;
@@ -232,7 +232,7 @@ bool DIALOG_CONFIGURE_PATHS::TransferDataFromWindow()
         {
             m_errorGrid = m_EnvVars;
             m_errorRow = row;
-            m_errorCol = EV_NAME_COL;
+            m_errorCol = TV_NAME_COL;
             m_errorMsg = _( "Environment variable name cannot be empty." );
             return false;
         }
@@ -240,7 +240,7 @@ bool DIALOG_CONFIGURE_PATHS::TransferDataFromWindow()
         {
             m_errorGrid = m_EnvVars;
             m_errorRow = row;
-            m_errorCol = EV_PATH_COL;
+            m_errorCol = TV_VALUE_COL;
             m_errorMsg = _( "Environment variable path cannot be empty." );
             return false;
         }
@@ -302,7 +302,7 @@ void DIALOG_CONFIGURE_PATHS::OnGridCellChanging( wxGridEvent& event )
     {
         if( grid == m_EnvVars )
         {
-            if( col == EV_NAME_COL )
+            if( col == TV_NAME_COL )
                 m_errorMsg = _( "Environment variable name cannot be empty." );
             else
                 m_errorMsg = _( "Environment variable path cannot be empty." );
@@ -323,7 +323,7 @@ void DIALOG_CONFIGURE_PATHS::OnGridCellChanging( wxGridEvent& event )
 
     if( grid == m_EnvVars )
     {
-        if( col == EV_PATH_COL && m_EnvVars->GetCellValue( row, EV_FLAG_COL ).Length() )
+        if( col == TV_VALUE_COL && m_EnvVars->GetCellValue( row, TV_FLAG_COL ).Length() )
         {
             wxString msg1 = _( "This path was defined  externally to the running process and\n"
                                "will only be temporarily overwritten." );
@@ -337,7 +337,7 @@ void DIALOG_CONFIGURE_PATHS::OnGridCellChanging( wxGridEvent& event )
             dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
             dlg.ShowModal();
         }
-        else if( col == EV_NAME_COL && m_EnvVars->GetCellValue( row, EV_NAME_COL ) != text )
+        else if( col == TV_NAME_COL && m_EnvVars->GetCellValue( row, TV_NAME_COL ) != text )
         {
             if( text == PROJECT_VAR_NAME )    // This env var name is reserved and cannot be added here:
             {
@@ -347,7 +347,7 @@ void DIALOG_CONFIGURE_PATHS::OnGridCellChanging( wxGridEvent& event )
                 event.Veto();
             }
             else    // Changing name; clear external flag
-                m_EnvVars->SetCellValue( row, EV_FLAG_COL, wxEmptyString );
+                m_EnvVars->SetCellValue( row, TV_FLAG_COL, wxEmptyString );
         }
     }
 }
@@ -360,8 +360,8 @@ void DIALOG_CONFIGURE_PATHS::OnAddEnvVar( wxCommandEvent& event )
 
     AppendEnvVar( wxEmptyString, wxEmptyString, false );
 
-    m_EnvVars->MakeCellVisible( m_EnvVars->GetNumberRows() - 1, EV_NAME_COL );
-    m_EnvVars->SetGridCursor( m_EnvVars->GetNumberRows() - 1, EV_NAME_COL );
+    m_EnvVars->MakeCellVisible( m_EnvVars->GetNumberRows() - 1, TV_NAME_COL );
+    m_EnvVars->SetGridCursor( m_EnvVars->GetNumberRows() - 1, TV_NAME_COL );
 
     m_EnvVars->EnableCellEditControl( true );
     m_EnvVars->ShowCellEditControl();
@@ -389,7 +389,7 @@ void DIALOG_CONFIGURE_PATHS::OnRemoveEnvVar( wxCommandEvent& event )
 
     if( curRow < 0 || m_EnvVars->GetNumberRows() <= curRow )
         return;
-    else if( IsEnvVarImmutable( m_EnvVars->GetCellValue( curRow, EV_NAME_COL ) ) )
+    else if( IsEnvVarImmutable( m_EnvVars->GetCellValue( curRow, TV_NAME_COL ) ) )
     {
         wxBell();
         return;
@@ -468,9 +468,9 @@ void DIALOG_CONFIGURE_PATHS::OnSearchPathMoveDown( wxCommandEvent& event )
 
 void DIALOG_CONFIGURE_PATHS::OnGridCellRightClick( wxGridEvent& aEvent )
 {
-    wxASSERT( (int) EV_PATH_COL == (int) SP_PATH_COL );
+    wxASSERT((int) TV_VALUE_COL == (int) SP_PATH_COL );
 
-    if( aEvent.GetCol() == EV_PATH_COL )
+    if( aEvent.GetCol() == TV_VALUE_COL )
     {
         wxMenu menu;
 
@@ -483,7 +483,7 @@ void DIALOG_CONFIGURE_PATHS::OnGridCellRightClick( wxGridEvent& aEvent )
             if( dlg.ShowModal() == wxID_OK )
             {
                 wxGrid* grid = dynamic_cast<wxGrid*>( aEvent.GetEventObject() );
-                grid->SetCellValue( aEvent.GetRow(), EV_PATH_COL, dlg.GetPath() );
+                grid->SetCellValue( aEvent.GetRow(), TV_VALUE_COL, dlg.GetPath() );
                 m_curdir = dlg.GetPath();
             }
         }
@@ -506,10 +506,10 @@ void DIALOG_CONFIGURE_PATHS::OnUpdateUI( wxUpdateUIEvent& event )
     {
         int width = m_EnvVars->GetClientRect().GetWidth();
 
-        m_EnvVars->AutoSizeColumn( EV_NAME_COL );
-        m_EnvVars->SetColSize( EV_NAME_COL, std::max( m_EnvVars->GetColSize( EV_NAME_COL ), 120 ) );
+        m_EnvVars->AutoSizeColumn( TV_NAME_COL );
+        m_EnvVars->SetColSize( TV_NAME_COL, std::max( m_EnvVars->GetColSize( TV_NAME_COL ), 120 ) );
 
-        m_EnvVars->SetColSize( EV_PATH_COL, width - m_EnvVars->GetColSize( EV_NAME_COL ) );
+        m_EnvVars->SetColSize( TV_VALUE_COL, width - m_EnvVars->GetColSize( TV_NAME_COL ) );
 
         width = m_SearchPaths->GetClientRect().GetWidth();
 
diff --git a/common/dialogs/dialog_page_settings.cpp b/common/dialogs/dialog_page_settings.cpp
index 3130c282fb..5647f715a7 100644
--- a/common/dialogs/dialog_page_settings.cpp
+++ b/common/dialogs/dialog_page_settings.cpp
@@ -758,7 +758,8 @@ void DIALOG_PAGES_SETTINGS::UpdatePageLayoutExample()
         WS_DATA_MODEL::SetAltInstance( m_pagelayout );
         GRFilledRect( NULL, &memDC, 0, 0, m_layout_size.x, m_layout_size.y, WHITE, WHITE );
         PrintPageLayout( &memDC, pageDUMMY, emptyString, emptyString, m_tb,
-                         m_screen->m_NumberOfScreens, m_screen->m_ScreenNumber, 1, 1, RED );
+                         m_screen->m_NumberOfScreens, m_screen->m_ScreenNumber, 1, 1, RED,
+                         &Prj() );
 
         memDC.SelectObject( wxNullBitmap );
         m_PageLayoutExampleBitmap->SetBitmap( *m_page_bitmap );
diff --git a/common/dialogs/panel_text_variables.cpp b/common/dialogs/panel_text_variables.cpp
new file mode 100644
index 0000000000..c92aae142b
--- /dev/null
+++ b/common/dialogs/panel_text_variables.cpp
@@ -0,0 +1,229 @@
+/*
+ * This program source code file is part of KICAD, a free EDA CAD application.
+ *
+ * Copyright (C) 2020 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program 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 2
+ * 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, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include <panel_text_variables.h>
+
+#include <bitmaps.h>
+#include <confirm.h>
+#include <validators.h>
+#include <project.h>
+#include <grid_tricks.h>
+#include <widgets/wx_grid.h>
+
+enum TEXT_VAR_GRID_COLUMNS
+{
+    TV_NAME_COL = 0,
+    TV_VALUE_COL
+};
+
+
+PANEL_TEXT_VARIABLES::PANEL_TEXT_VARIABLES( wxWindow* aParent, PROJECT* aProject ) :
+    PANEL_TEXT_VARIABLES_BASE( aParent ),
+    m_project( aProject ),
+    m_errorRow( -1 ), m_errorCol( -1 ),
+    m_gridWidthsDirty( true )
+{
+    m_btnAddTextVar->SetBitmap( KiBitmap( small_plus_xpm ) );
+    m_btnDeleteTextVar->SetBitmap( KiBitmap( trash_xpm ) );
+
+    m_TextVars->DeleteRows( 0, m_TextVars->GetNumberRows() );
+
+    // prohibit these characters in the alias names: []{}()%~<>"='`;:.,&?/\|$
+    m_nameValidator.SetStyle( wxFILTER_EXCLUDE_CHAR_LIST );
+    m_nameValidator.SetCharExcludes( wxT( "{}[]()%~<>\"='`;:.,&?/\\|$" ) );
+
+    m_TextVars->PushEventHandler( new GRID_TRICKS( m_TextVars ) );
+    m_TextVars->SetSelectionMode( wxGrid::wxGridSelectionModes::wxGridSelectRows );
+
+    // wxFormBuilder doesn't include this event...
+    m_TextVars->Connect( wxEVT_GRID_CELL_CHANGING, wxGridEventHandler( PANEL_TEXT_VARIABLES::OnGridCellChanging ), NULL, this );
+}
+
+
+PANEL_TEXT_VARIABLES::~PANEL_TEXT_VARIABLES()
+{
+    // Delete the GRID_TRICKS.
+    m_TextVars->PopEventHandler( true );
+
+    m_TextVars->Disconnect( wxEVT_GRID_CELL_CHANGING, wxGridEventHandler( PANEL_TEXT_VARIABLES::OnGridCellChanging ), NULL, this );
+}
+
+
+bool PANEL_TEXT_VARIABLES::TransferDataToWindow()
+{
+    std::map<wxString, wxString>& variables = m_project->GetTextVars();
+
+    for( const auto& var : variables )
+        AppendTextVar( var.first, var.second );
+
+    return true;
+}
+
+
+void PANEL_TEXT_VARIABLES::AppendTextVar( const wxString& aName, const wxString& aValue )
+{
+    int i = m_TextVars->GetNumberRows();
+
+    m_TextVars->AppendRows( 1 );
+
+    m_TextVars->SetCellValue( i, TV_NAME_COL, aName );
+
+    wxGridCellAttr* nameCellAttr = m_TextVars->GetOrCreateCellAttr( i, TV_NAME_COL );
+    wxGridCellTextEditor* nameTextEditor = new GRID_CELL_TEXT_EDITOR();
+    nameTextEditor->SetValidator( m_nameValidator );
+    nameCellAttr->SetEditor( nameTextEditor );
+    nameCellAttr->DecRef();
+
+    m_TextVars->SetCellValue( i, TV_VALUE_COL, aValue );
+}
+
+
+bool PANEL_TEXT_VARIABLES::TransferDataFromWindow()
+{
+    if( !m_TextVars->CommitPendingChanges() )
+        return false;
+
+    for( int row = 0; row < m_TextVars->GetNumberRows(); ++row )
+    {
+        if( m_TextVars->GetCellValue( row, TV_NAME_COL ).IsEmpty() )
+        {
+            m_errorRow = row;
+            m_errorCol = TV_NAME_COL;
+            m_errorMsg = _( "Variable name cannot be empty." );
+            return false;
+        }
+    }
+
+    std::map<wxString, wxString>& variables = m_project->GetTextVars();
+
+    variables.clear();
+
+    for( int row = 0; row < m_TextVars->GetNumberRows(); ++row )
+    {
+        wxString name = m_TextVars->GetCellValue( row, TV_NAME_COL );
+        wxString value = m_TextVars->GetCellValue( row, TV_VALUE_COL );
+        variables[ name ] = value;
+    }
+
+    return true;
+}
+
+
+void PANEL_TEXT_VARIABLES::OnGridCellChanging( wxGridEvent& event )
+{
+    int      row = event.GetRow();
+    int      col = event.GetCol();
+    wxString text = event.GetString();
+
+    if( text.IsEmpty() && col == TV_NAME_COL )
+    {
+        m_errorMsg = _( "Variable name cannot be empty." );
+        m_errorRow = row;
+        m_errorCol = col;
+
+        event.Veto();
+    }
+}
+
+
+void PANEL_TEXT_VARIABLES::OnAddTextVar( wxCommandEvent& event )
+{
+    if( !m_TextVars->CommitPendingChanges() )
+        return;
+
+    AppendTextVar( wxEmptyString, wxEmptyString );
+
+    m_TextVars->MakeCellVisible( m_TextVars->GetNumberRows() - 1, TV_NAME_COL );
+    m_TextVars->SetGridCursor( m_TextVars->GetNumberRows() - 1, TV_NAME_COL );
+
+    m_TextVars->EnableCellEditControl( true );
+    m_TextVars->ShowCellEditControl();
+}
+
+
+void PANEL_TEXT_VARIABLES::OnRemoveTextVar( wxCommandEvent& event )
+{
+    int curRow = m_TextVars->GetGridCursorRow();
+
+    if( curRow < 0 || m_TextVars->GetNumberRows() <= curRow )
+        return;
+
+    m_TextVars->CommitPendingChanges( true /* silent mode; we don't care if it's valid */ );
+    m_TextVars->DeleteRows( curRow, 1 );
+
+    m_TextVars->MakeCellVisible( std::max( 0, curRow-1 ), m_TextVars->GetGridCursorCol() );
+    m_TextVars->SetGridCursor( std::max( 0, curRow-1 ), m_TextVars->GetGridCursorCol() );
+}
+
+
+void PANEL_TEXT_VARIABLES::OnGridCellChange( wxGridEvent& aEvent )
+{
+    m_gridWidthsDirty = true;
+
+    aEvent.Skip();
+}
+
+
+void PANEL_TEXT_VARIABLES::OnUpdateUI( wxUpdateUIEvent& event )
+{
+    if( m_gridWidthsDirty && ( !m_TextVars->IsCellEditControlShown() ) )
+    {
+        int width = m_TextVars->GetClientRect().GetWidth();
+
+        m_TextVars->AutoSizeColumn( TV_NAME_COL );
+        m_TextVars->SetColSize( TV_NAME_COL, std::max( m_TextVars->GetColSize( TV_NAME_COL ), 120 ) );
+
+        m_TextVars->SetColSize( TV_VALUE_COL, width - m_TextVars->GetColSize( TV_NAME_COL ) );
+        m_gridWidthsDirty = false;
+    }
+
+    // Handle a grid error.  This is delayed to OnUpdateUI so that we can change focus
+    // even when the original validation was triggered from a killFocus event (and for
+    // dialog with notebooks, so that the corresponding notebook page can be shown in
+    // the background when triggered from an OK).
+    if( !m_errorMsg.IsEmpty() )
+    {
+        // We will re-enter this routine when the error dialog is displayed, so make
+        // sure we don't keep putting up more dialogs.
+        wxString errorMsg = m_errorMsg;
+        m_errorMsg = wxEmptyString;
+
+        DisplayErrorMessage( this, errorMsg );
+
+        m_TextVars->SetFocus();
+        m_TextVars->MakeCellVisible( m_errorRow, m_errorCol );
+        m_TextVars->SetGridCursor( m_errorRow, m_errorCol );
+
+        m_TextVars->EnableCellEditControl( true );
+        m_TextVars->ShowCellEditControl();
+    }
+}
+
+
+void PANEL_TEXT_VARIABLES::OnGridSize( wxSizeEvent& event )
+{
+    m_gridWidthsDirty = true;
+
+    event.Skip();
+}
+
diff --git a/common/dialogs/panel_text_variables_base.cpp b/common/dialogs/panel_text_variables_base.cpp
new file mode 100644
index 0000000000..78d170c012
--- /dev/null
+++ b/common/dialogs/panel_text_variables_base.cpp
@@ -0,0 +1,98 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#include "widgets/wx_grid.h"
+
+#include "panel_text_variables_base.h"
+
+///////////////////////////////////////////////////////////////////////////
+
+PANEL_TEXT_VARIABLES_BASE::PANEL_TEXT_VARIABLES_BASE( wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style, const wxString& name ) : wxPanel( parent, id, pos, size, style, name )
+{
+	wxBoxSizer* bPanelSizer;
+	bPanelSizer = new wxBoxSizer( wxVERTICAL );
+
+	wxBoxSizer* bSizer3;
+	bSizer3 = new wxBoxSizer( wxVERTICAL );
+
+	m_TextVars = new WX_GRID( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 );
+
+	// Grid
+	m_TextVars->CreateGrid( 1, 2 );
+	m_TextVars->EnableEditing( true );
+	m_TextVars->EnableGridLines( true );
+	m_TextVars->EnableDragGridSize( false );
+	m_TextVars->SetMargins( 0, 0 );
+
+	// Columns
+	m_TextVars->SetColSize( 0, 150 );
+	m_TextVars->SetColSize( 1, 454 );
+	m_TextVars->EnableDragColMove( false );
+	m_TextVars->EnableDragColSize( true );
+	m_TextVars->SetColLabelSize( 22 );
+	m_TextVars->SetColLabelValue( 0, _("Variable Name") );
+	m_TextVars->SetColLabelValue( 1, _("Text Substitution") );
+	m_TextVars->SetColLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER );
+
+	// Rows
+	m_TextVars->EnableDragRowSize( true );
+	m_TextVars->SetRowLabelSize( 0 );
+	m_TextVars->SetRowLabelAlignment( wxALIGN_CENTER, wxALIGN_CENTER );
+
+	// Label Appearance
+
+	// Cell Defaults
+	m_TextVars->SetDefaultCellAlignment( wxALIGN_LEFT, wxALIGN_TOP );
+	m_TextVars->SetMinSize( wxSize( 604,170 ) );
+
+	bSizer3->Add( m_TextVars, 1, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 );
+
+	wxBoxSizer* bSizerEnvVarBtns;
+	bSizerEnvVarBtns = new wxBoxSizer( wxHORIZONTAL );
+
+	m_btnAddTextVar = new wxBitmapButton( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 );
+	m_btnAddTextVar->SetMinSize( wxSize( 30,29 ) );
+
+	bSizerEnvVarBtns->Add( m_btnAddTextVar, 0, wxRIGHT, 5 );
+
+
+	bSizerEnvVarBtns->Add( 0, 0, 0, wxEXPAND|wxRIGHT|wxLEFT, 5 );
+
+	m_btnDeleteTextVar = new wxBitmapButton( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW|0 );
+	m_btnDeleteTextVar->SetMinSize( wxSize( 30,29 ) );
+
+	bSizerEnvVarBtns->Add( m_btnDeleteTextVar, 0, wxRIGHT|wxLEFT, 5 );
+
+
+	bSizer3->Add( bSizerEnvVarBtns, 0, wxEXPAND|wxRIGHT|wxLEFT, 5 );
+
+
+	bPanelSizer->Add( bSizer3, 1, wxEXPAND|wxTOP|wxLEFT, 10 );
+
+
+	this->SetSizer( bPanelSizer );
+	this->Layout();
+	bPanelSizer->Fit( this );
+
+	// Connect Events
+	this->Connect( wxEVT_UPDATE_UI, wxUpdateUIEventHandler( PANEL_TEXT_VARIABLES_BASE::OnUpdateUI ) );
+	m_TextVars->Connect( wxEVT_GRID_CELL_CHANGED, wxGridEventHandler( PANEL_TEXT_VARIABLES_BASE::OnGridCellChange ), NULL, this );
+	m_TextVars->Connect( wxEVT_SIZE, wxSizeEventHandler( PANEL_TEXT_VARIABLES_BASE::OnGridSize ), NULL, this );
+	m_btnAddTextVar->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_TEXT_VARIABLES_BASE::OnAddTextVar ), NULL, this );
+	m_btnDeleteTextVar->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_TEXT_VARIABLES_BASE::OnRemoveTextVar ), NULL, this );
+}
+
+PANEL_TEXT_VARIABLES_BASE::~PANEL_TEXT_VARIABLES_BASE()
+{
+	// Disconnect Events
+	this->Disconnect( wxEVT_UPDATE_UI, wxUpdateUIEventHandler( PANEL_TEXT_VARIABLES_BASE::OnUpdateUI ) );
+	m_TextVars->Disconnect( wxEVT_GRID_CELL_CHANGED, wxGridEventHandler( PANEL_TEXT_VARIABLES_BASE::OnGridCellChange ), NULL, this );
+	m_TextVars->Disconnect( wxEVT_SIZE, wxSizeEventHandler( PANEL_TEXT_VARIABLES_BASE::OnGridSize ), NULL, this );
+	m_btnAddTextVar->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_TEXT_VARIABLES_BASE::OnAddTextVar ), NULL, this );
+	m_btnDeleteTextVar->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PANEL_TEXT_VARIABLES_BASE::OnRemoveTextVar ), NULL, this );
+
+}
diff --git a/common/dialogs/panel_text_variables_base.fbp b/common/dialogs/panel_text_variables_base.fbp
new file mode 100644
index 0000000000..7539fd94e5
--- /dev/null
+++ b/common/dialogs/panel_text_variables_base.fbp
@@ -0,0 +1,329 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<wxFormBuilder_Project>
+    <FileVersion major="1" minor="15" />
+    <object class="Project" expanded="1">
+        <property name="class_decoration"></property>
+        <property name="code_generation">C++</property>
+        <property name="disconnect_events">1</property>
+        <property name="disconnect_mode">source_name</property>
+        <property name="disconnect_php_events">0</property>
+        <property name="disconnect_python_events">0</property>
+        <property name="embedded_files_path">res</property>
+        <property name="encoding">UTF-8</property>
+        <property name="event_generation">connect</property>
+        <property name="file">panel_text_variables_base</property>
+        <property name="first_id">1000</property>
+        <property name="help_provider">none</property>
+        <property name="indent_with_spaces"></property>
+        <property name="internationalize">1</property>
+        <property name="name">PanelTextVariables</property>
+        <property name="namespace"></property>
+        <property name="path">.</property>
+        <property name="precompiled_header"></property>
+        <property name="relative_path">1</property>
+        <property name="skip_lua_events">1</property>
+        <property name="skip_php_events">1</property>
+        <property name="skip_python_events">1</property>
+        <property name="ui_table">UI</property>
+        <property name="use_enum">1</property>
+        <property name="use_microsoft_bom">0</property>
+        <object class="Panel" expanded="1">
+            <property name="aui_managed">0</property>
+            <property name="aui_manager_style">wxAUI_MGR_DEFAULT</property>
+            <property name="bg"></property>
+            <property name="context_help"></property>
+            <property name="context_menu">1</property>
+            <property name="enabled">1</property>
+            <property name="event_handler">impl_virtual</property>
+            <property name="fg"></property>
+            <property name="font"></property>
+            <property name="hidden">0</property>
+            <property name="id">wxID_ANY</property>
+            <property name="maximum_size"></property>
+            <property name="minimum_size"></property>
+            <property name="name">PANEL_TEXT_VARIABLES_BASE</property>
+            <property name="pos"></property>
+            <property name="size">-1,-1</property>
+            <property name="subclass">; forward_declare</property>
+            <property name="tooltip"></property>
+            <property name="window_extra_style"></property>
+            <property name="window_name"></property>
+            <property name="window_style">wxTAB_TRAVERSAL</property>
+            <event name="OnUpdateUI">OnUpdateUI</event>
+            <object class="wxBoxSizer" expanded="1">
+                <property name="minimum_size"></property>
+                <property name="name">bPanelSizer</property>
+                <property name="orient">wxVERTICAL</property>
+                <property name="permission">none</property>
+                <object class="sizeritem" expanded="1">
+                    <property name="border">10</property>
+                    <property name="flag">wxEXPAND|wxTOP|wxLEFT</property>
+                    <property name="proportion">1</property>
+                    <object class="wxBoxSizer" expanded="1">
+                        <property name="minimum_size"></property>
+                        <property name="name">bSizer3</property>
+                        <property name="orient">wxVERTICAL</property>
+                        <property name="permission">none</property>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">5</property>
+                            <property name="flag">wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT</property>
+                            <property name="proportion">1</property>
+                            <object class="wxGrid" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="autosize_cols">0</property>
+                                <property name="autosize_rows">0</property>
+                                <property name="best_size"></property>
+                                <property name="bg"></property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="cell_bg"></property>
+                                <property name="cell_font"></property>
+                                <property name="cell_horiz_alignment">wxALIGN_LEFT</property>
+                                <property name="cell_text"></property>
+                                <property name="cell_vert_alignment">wxALIGN_TOP</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="col_label_horiz_alignment">wxALIGN_CENTER</property>
+                                <property name="col_label_size">22</property>
+                                <property name="col_label_values">&quot;Variable Name&quot; &quot;Text Substitution&quot;</property>
+                                <property name="col_label_vert_alignment">wxALIGN_CENTER</property>
+                                <property name="cols">2</property>
+                                <property name="column_sizes">150,454</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="default_pane">0</property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="drag_col_move">0</property>
+                                <property name="drag_col_size">1</property>
+                                <property name="drag_grid_size">0</property>
+                                <property name="drag_row_size">1</property>
+                                <property name="editing">1</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="font"></property>
+                                <property name="grid_line_color"></property>
+                                <property name="grid_lines">1</property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="label_bg"></property>
+                                <property name="label_font"></property>
+                                <property name="label_text"></property>
+                                <property name="margin_height">0</property>
+                                <property name="margin_width">0</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size">604,170</property>
+                                <property name="moveable">1</property>
+                                <property name="name">m_TextVars</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="row_label_horiz_alignment">wxALIGN_CENTER</property>
+                                <property name="row_label_size">0</property>
+                                <property name="row_label_values"></property>
+                                <property name="row_label_vert_alignment">wxALIGN_CENTER</property>
+                                <property name="row_sizes"></property>
+                                <property name="rows">1</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="subclass">WX_GRID; widgets/wx_grid.h; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style"></property>
+                                <event name="OnGridCellChange">OnGridCellChange</event>
+                                <event name="OnSize">OnGridSize</event>
+                            </object>
+                        </object>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">5</property>
+                            <property name="flag">wxEXPAND|wxRIGHT|wxLEFT</property>
+                            <property name="proportion">0</property>
+                            <object class="wxBoxSizer" expanded="1">
+                                <property name="minimum_size"></property>
+                                <property name="name">bSizerEnvVarBtns</property>
+                                <property name="orient">wxHORIZONTAL</property>
+                                <property name="permission">none</property>
+                                <object class="sizeritem" expanded="1">
+                                    <property name="border">5</property>
+                                    <property name="flag">wxRIGHT</property>
+                                    <property name="proportion">0</property>
+                                    <object class="wxBitmapButton" expanded="1">
+                                        <property name="BottomDockable">1</property>
+                                        <property name="LeftDockable">1</property>
+                                        <property name="RightDockable">1</property>
+                                        <property name="TopDockable">1</property>
+                                        <property name="aui_layer"></property>
+                                        <property name="aui_name"></property>
+                                        <property name="aui_position"></property>
+                                        <property name="aui_row"></property>
+                                        <property name="best_size"></property>
+                                        <property name="bg"></property>
+                                        <property name="bitmap"></property>
+                                        <property name="caption"></property>
+                                        <property name="caption_visible">1</property>
+                                        <property name="center_pane">0</property>
+                                        <property name="close_button">1</property>
+                                        <property name="context_help"></property>
+                                        <property name="context_menu">1</property>
+                                        <property name="current"></property>
+                                        <property name="default">0</property>
+                                        <property name="default_pane">0</property>
+                                        <property name="disabled"></property>
+                                        <property name="dock">Dock</property>
+                                        <property name="dock_fixed">0</property>
+                                        <property name="docking">Left</property>
+                                        <property name="enabled">1</property>
+                                        <property name="fg"></property>
+                                        <property name="floatable">1</property>
+                                        <property name="focus"></property>
+                                        <property name="font"></property>
+                                        <property name="gripper">0</property>
+                                        <property name="hidden">0</property>
+                                        <property name="id">wxID_ANY</property>
+                                        <property name="label">Add Text Variable</property>
+                                        <property name="margins"></property>
+                                        <property name="markup">0</property>
+                                        <property name="max_size"></property>
+                                        <property name="maximize_button">0</property>
+                                        <property name="maximum_size"></property>
+                                        <property name="min_size"></property>
+                                        <property name="minimize_button">0</property>
+                                        <property name="minimum_size">30,29</property>
+                                        <property name="moveable">1</property>
+                                        <property name="name">m_btnAddTextVar</property>
+                                        <property name="pane_border">1</property>
+                                        <property name="pane_position"></property>
+                                        <property name="pane_size"></property>
+                                        <property name="permission">protected</property>
+                                        <property name="pin_button">1</property>
+                                        <property name="pos"></property>
+                                        <property name="position"></property>
+                                        <property name="pressed"></property>
+                                        <property name="resize">Resizable</property>
+                                        <property name="show">1</property>
+                                        <property name="size"></property>
+                                        <property name="style"></property>
+                                        <property name="subclass">; forward_declare</property>
+                                        <property name="toolbar_pane">0</property>
+                                        <property name="tooltip"></property>
+                                        <property name="validator_data_type"></property>
+                                        <property name="validator_style">wxFILTER_NONE</property>
+                                        <property name="validator_type">wxDefaultValidator</property>
+                                        <property name="validator_variable"></property>
+                                        <property name="window_extra_style"></property>
+                                        <property name="window_name"></property>
+                                        <property name="window_style"></property>
+                                        <event name="OnButtonClick">OnAddTextVar</event>
+                                    </object>
+                                </object>
+                                <object class="sizeritem" expanded="1">
+                                    <property name="border">5</property>
+                                    <property name="flag">wxEXPAND|wxRIGHT|wxLEFT</property>
+                                    <property name="proportion">0</property>
+                                    <object class="spacer" expanded="1">
+                                        <property name="height">0</property>
+                                        <property name="permission">protected</property>
+                                        <property name="width">0</property>
+                                    </object>
+                                </object>
+                                <object class="sizeritem" expanded="1">
+                                    <property name="border">5</property>
+                                    <property name="flag">wxRIGHT|wxLEFT</property>
+                                    <property name="proportion">0</property>
+                                    <object class="wxBitmapButton" expanded="1">
+                                        <property name="BottomDockable">1</property>
+                                        <property name="LeftDockable">1</property>
+                                        <property name="RightDockable">1</property>
+                                        <property name="TopDockable">1</property>
+                                        <property name="aui_layer"></property>
+                                        <property name="aui_name"></property>
+                                        <property name="aui_position"></property>
+                                        <property name="aui_row"></property>
+                                        <property name="best_size"></property>
+                                        <property name="bg"></property>
+                                        <property name="bitmap"></property>
+                                        <property name="caption"></property>
+                                        <property name="caption_visible">1</property>
+                                        <property name="center_pane">0</property>
+                                        <property name="close_button">1</property>
+                                        <property name="context_help"></property>
+                                        <property name="context_menu">1</property>
+                                        <property name="current"></property>
+                                        <property name="default">0</property>
+                                        <property name="default_pane">0</property>
+                                        <property name="disabled"></property>
+                                        <property name="dock">Dock</property>
+                                        <property name="dock_fixed">0</property>
+                                        <property name="docking">Left</property>
+                                        <property name="enabled">1</property>
+                                        <property name="fg"></property>
+                                        <property name="floatable">1</property>
+                                        <property name="focus"></property>
+                                        <property name="font"></property>
+                                        <property name="gripper">0</property>
+                                        <property name="hidden">0</property>
+                                        <property name="id">wxID_ANY</property>
+                                        <property name="label">Delete Text Variable</property>
+                                        <property name="margins"></property>
+                                        <property name="markup">0</property>
+                                        <property name="max_size"></property>
+                                        <property name="maximize_button">0</property>
+                                        <property name="maximum_size"></property>
+                                        <property name="min_size"></property>
+                                        <property name="minimize_button">0</property>
+                                        <property name="minimum_size">30,29</property>
+                                        <property name="moveable">1</property>
+                                        <property name="name">m_btnDeleteTextVar</property>
+                                        <property name="pane_border">1</property>
+                                        <property name="pane_position"></property>
+                                        <property name="pane_size"></property>
+                                        <property name="permission">protected</property>
+                                        <property name="pin_button">1</property>
+                                        <property name="pos"></property>
+                                        <property name="position"></property>
+                                        <property name="pressed"></property>
+                                        <property name="resize">Resizable</property>
+                                        <property name="show">1</property>
+                                        <property name="size"></property>
+                                        <property name="style"></property>
+                                        <property name="subclass">; forward_declare</property>
+                                        <property name="toolbar_pane">0</property>
+                                        <property name="tooltip"></property>
+                                        <property name="validator_data_type"></property>
+                                        <property name="validator_style">wxFILTER_NONE</property>
+                                        <property name="validator_type">wxDefaultValidator</property>
+                                        <property name="validator_variable"></property>
+                                        <property name="window_extra_style"></property>
+                                        <property name="window_name"></property>
+                                        <property name="window_style"></property>
+                                        <event name="OnButtonClick">OnRemoveTextVar</event>
+                                    </object>
+                                </object>
+                            </object>
+                        </object>
+                    </object>
+                </object>
+            </object>
+        </object>
+    </object>
+</wxFormBuilder_Project>
diff --git a/common/dialogs/panel_text_variables_base.h b/common/dialogs/panel_text_variables_base.h
new file mode 100644
index 0000000000..0a8b881069
--- /dev/null
+++ b/common/dialogs/panel_text_variables_base.h
@@ -0,0 +1,57 @@
+///////////////////////////////////////////////////////////////////////////
+// C++ code generated with wxFormBuilder (version Oct 26 2018)
+// http://www.wxformbuilder.org/
+//
+// PLEASE DO *NOT* EDIT THIS FILE!
+///////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include <wx/artprov.h>
+#include <wx/xrc/xmlres.h>
+#include <wx/intl.h>
+class WX_GRID;
+
+#include <wx/colour.h>
+#include <wx/settings.h>
+#include <wx/string.h>
+#include <wx/font.h>
+#include <wx/grid.h>
+#include <wx/gdicmn.h>
+#include <wx/bmpbuttn.h>
+#include <wx/bitmap.h>
+#include <wx/image.h>
+#include <wx/icon.h>
+#include <wx/button.h>
+#include <wx/sizer.h>
+#include <wx/panel.h>
+
+///////////////////////////////////////////////////////////////////////////
+
+///////////////////////////////////////////////////////////////////////////////
+/// Class PANEL_TEXT_VARIABLES_BASE
+///////////////////////////////////////////////////////////////////////////////
+class PANEL_TEXT_VARIABLES_BASE : public wxPanel
+{
+	private:
+
+	protected:
+		WX_GRID* m_TextVars;
+		wxBitmapButton* m_btnAddTextVar;
+		wxBitmapButton* m_btnDeleteTextVar;
+
+		// Virtual event handlers, overide them in your derived class
+		virtual void OnUpdateUI( wxUpdateUIEvent& event ) { event.Skip(); }
+		virtual void OnGridCellChange( wxGridEvent& event ) { event.Skip(); }
+		virtual void OnGridSize( wxSizeEvent& event ) { event.Skip(); }
+		virtual void OnAddTextVar( wxCommandEvent& event ) { event.Skip(); }
+		virtual void OnRemoveTextVar( wxCommandEvent& event ) { event.Skip(); }
+
+
+	public:
+
+		PANEL_TEXT_VARIABLES_BASE( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( -1,-1 ), long style = wxTAB_TRAVERSAL, const wxString& name = wxEmptyString );
+		~PANEL_TEXT_VARIABLES_BASE();
+
+};
+
diff --git a/common/eda_draw_frame.cpp b/common/eda_draw_frame.cpp
index 70f4066ae7..cfb369994a 100644
--- a/common/eda_draw_frame.cpp
+++ b/common/eda_draw_frame.cpp
@@ -731,7 +731,7 @@ static const wxString productName = wxT( "KiCad E.D.A.  " );
 void PrintPageLayout( wxDC* aDC, const PAGE_INFO& aPageInfo, const wxString& aFullSheetName,
                       const wxString& aFileName, const TITLE_BLOCK& aTitleBlock, int aSheetCount,
                       int aSheetNumber, int aPenWidth, double aScalar, COLOR4D aColor,
-                      const wxString& aSheetLayer )
+                      const PROJECT* aProject, const wxString& aSheetLayer )
 {
     WS_DRAW_ITEM_LIST drawList;
 
@@ -742,6 +742,7 @@ void PrintPageLayout( wxDC* aDC, const PAGE_INFO& aPageInfo, const wxString& aFu
     drawList.SetFileName( aFileName );
     drawList.SetSheetName( aFullSheetName );
     drawList.SetSheetLayer( aSheetLayer );
+    drawList.SetProject( aProject );
 
     drawList.BuildWorkSheetGraphicList( aPageInfo, aTitleBlock );
 
@@ -769,7 +770,7 @@ void EDA_DRAW_FRAME::PrintWorkSheet( wxDC* aDC, BASE_SCREEN* aScreen, int aLineW
 
     PrintPageLayout( aDC, GetPageSettings(), GetScreenDesc(), aFilename, GetTitleBlock(),
                      aScreen->m_NumberOfScreens, aScreen->m_ScreenNumber, aLineWidth, aScalar,
-                     color, aSheetLayer );
+                     color, &Prj(), aSheetLayer );
 
     if( origin.y > 0 )
     {
diff --git a/common/page_layout/page_layout_reader.cpp b/common/page_layout/page_layout_reader.cpp
index dfc94becf2..7c67e84b68 100644
--- a/common/page_layout/page_layout_reader.cpp
+++ b/common/page_layout/page_layout_reader.cpp
@@ -113,6 +113,80 @@ PAGE_LAYOUT_READER_PARSER::PAGE_LAYOUT_READER_PARSER( const char* aLine, const w
 }
 
 
+wxString convertLegacyVariableRefs( const wxString& aTextbase )
+{
+    wxString msg;
+
+    /*
+     * Legacy formats
+     * %% = replaced by %
+     * %K = Kicad version
+     * %Z = paper format name (A4, USLetter)
+     * %Y = company name
+     * %D = date
+     * %R = revision
+     * %S = sheet number
+     * %N = number of sheets
+     * %L = layer name
+     * %Cx = comment (x = 0 to 9 to identify the comment)
+     * %F = filename
+     * %P = sheet path (sheet full name)
+     * %T = title
+     */
+
+    for( unsigned ii = 0; ii < aTextbase.Len(); ii++ )
+    {
+        if( aTextbase[ii] != '%' )
+        {
+            msg << aTextbase[ii];
+            continue;
+        }
+
+        if( ++ii >= aTextbase.Len() )
+            break;
+
+        wxChar format = aTextbase[ii];
+
+        switch( format )
+        {
+            case '%': msg += '%';                       break;
+            case 'D': msg += wxT( "${ISSUE_DATE}" );    break;
+            case 'R': msg += wxT( "${REVISION}" );      break;
+            case 'K': msg += wxT( "${KICAD_VERSION}" ); break;
+            case 'Z': msg += wxT( "${PAPER}" );         break;
+            case 'S': msg += wxT( "${#}" );             break;
+            case 'N': msg += wxT( "${##}" );            break;
+            case 'F': msg += wxT( "${FILENAME}" );      break;
+            case 'L': msg += wxT( "${LAYER}" );         break;
+            case 'P': msg += wxT( "${SHEETNAME}" );     break;
+            case 'Y': msg += wxT( "${COMPANY}" );       break;
+            case 'T': msg += wxT( "${TITLE}" );         break;
+            case 'C':
+                format = aTextbase[++ii];
+
+                switch( format )
+                {
+                case '0': msg += wxT( "${COMMENT0}" );  break;
+                case '1': msg += wxT( "${COMMENT1}" );  break;
+                case '2': msg += wxT( "${COMMENT2}" );  break;
+                case '3': msg += wxT( "${COMMENT3}" );  break;
+                case '4': msg += wxT( "${COMMENT4}" );  break;
+                case '5': msg += wxT( "${COMMENT5}" );  break;
+                case '6': msg += wxT( "${COMMENT6}" );  break;
+                case '7': msg += wxT( "${COMMENT7}" );  break;
+                case '8': msg += wxT( "${COMMENT8}" );  break;
+                case '9': msg += wxT( "${COMMENT9}" );  break;
+                }
+
+            default:
+                break;
+        }
+    }
+
+    return msg;
+}
+
+
 void PAGE_LAYOUT_READER_PARSER::Parse( WS_DATA_MODEL* aLayout )
 {
     WS_DATA_ITEM* item;
@@ -158,7 +232,7 @@ void PAGE_LAYOUT_READER_PARSER::Parse( WS_DATA_MODEL* aLayout )
 
         case T_tbtext:
             NeedSYMBOLorNUMBER();
-            item = new WS_DATA_ITEM_TEXT( FromUTF8() );
+            item = new WS_DATA_ITEM_TEXT( convertLegacyVariableRefs( FromUTF8() ) );
             parseText( (WS_DATA_ITEM_TEXT*) item );
             aLayout->Append( item );
             break;
@@ -170,6 +244,7 @@ void PAGE_LAYOUT_READER_PARSER::Parse( WS_DATA_MODEL* aLayout )
     }
 }
 
+
 void PAGE_LAYOUT_READER_PARSER::parseSetup( WS_DATA_MODEL* aLayout )
 {
     for( T token = NextTok(); token != T_RIGHT && token != EOF; token = NextTok() )
diff --git a/common/page_layout/ws_painter.cpp b/common/page_layout/ws_painter.cpp
index 1dfc96c8c9..12cb56a33f 100644
--- a/common/page_layout/ws_painter.cpp
+++ b/common/page_layout/ws_painter.cpp
@@ -89,124 +89,93 @@ const COLOR4D& WS_RENDER_SETTINGS::GetColor( const VIEW_ITEM* aItem, int aLayer
 // after replacing format symbols by the corresponding value
 wxString WS_DRAW_ITEM_LIST::BuildFullText( const wxString& aTextbase )
 {
-    wxString msg;
+    auto wsResolver = [ this ]( wxString* token ) -> bool
+                      {
+                          if( token->IsSameAs( wxT( "KICAD_VERSION" ) ) )
+                          {
+                              *token = wxString::Format( wxT( "%s%s %s" ),
+                                                         productName,
+                                                         Pgm().App().GetAppName(),
+                                                         GetBuildVersion() );
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "#" ) ) )
+                          {
+                              *token = wxString::Format( wxT( "%d" ), m_sheetNumber );
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "##" ) ) )
+                          {
+                              *token = wxString::Format( wxT( "%d" ), m_sheetCount );
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "SHEETNAME" ) ) )
+                          {
+                              *token = m_sheetFullName;
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "FILENAME" ) ) )
+                          {
+                              wxFileName fn( m_fileName );
+                              *token = fn.GetFullName();
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "PAPER" ) ) )
+                          {
+                              *token = m_paperFormat ? *m_paperFormat : wxEmptyString;
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "LAYER" ) ) )
+                          {
+                              *token = m_sheetLayer ? *m_sheetLayer : wxEmptyString;
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "ISSUE_DATE" ) ) )
+                          {
+                              *token = m_titleBlock ? m_titleBlock->GetDate() : wxEmptyString;
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "REVISION" ) ) )
+                          {
+                              *token = m_titleBlock ? m_titleBlock->GetRevision() : wxEmptyString;
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "TITLE" ) ) )
+                          {
+                              *token = m_titleBlock ? m_titleBlock->GetTitle() : wxEmptyString;
+                              return true;
+                          }
+                          else if( token->IsSameAs( wxT( "COMPANY" ) ) )
+                          {
+                              *token = m_titleBlock ? m_titleBlock->GetCompany() : wxEmptyString;
+                              return true;
+                          }
+                          else if( token->Left( token->Len()-1 ).IsSameAs( wxT( "COMMENT" ) ) )
+                          {
+                              wxChar c = token->Last();
 
-    /* Known formats
-     * %% = replaced by %
-     * %K = Kicad version
-     * %Z = paper format name (A4, USLetter)
-     * %Y = company name
-     * %D = date
-     * %R = revision
-     * %S = sheet number
-     * %N = number of sheets
-     * %L = layer name
-     * %Cx = comment (x = 0 to 9 to identify the comment)
-     * %F = filename
-     * %P = sheet path (sheet full name)
-     * %T = title
-     */
+                              switch( c )
+                              {
+                              case '0':
+                              case '1':
+                              case '2':
+                              case '3':
+                              case '4':
+                              case '5':
+                              case '6':
+                              case '7':
+                              case '8':
+                              case '9':
+                                  *token = m_titleBlock ? m_titleBlock->GetComment( c - '0' )
+                                                        : wxEmptyString;
+                                  return true;
+                              }
+                          }
 
-    for( unsigned ii = 0; ii < aTextbase.Len(); ii++ )
-    {
-        if( aTextbase[ii] != '%' )
-        {
-            msg << aTextbase[ii];
-            continue;
-        }
+                          return false;
+                      };
 
-        if( ++ii >= aTextbase.Len() )
-            break;
-
-        wxChar format = aTextbase[ii];
-        switch( format )
-        {
-            case '%':
-                msg += '%';
-                break;
-
-            case 'D':
-                if( m_titleBlock )
-                    msg += m_titleBlock->GetDate();
-                break;
-
-            case 'R':
-                if( m_titleBlock )
-                    msg += m_titleBlock->GetRevision();
-                break;
-
-            case 'K':
-                msg += productName + Pgm().App().GetAppName();
-                msg += wxT( " " ) + GetBuildVersion();
-                break;
-
-            case 'Z':
-                if( m_paperFormat )
-                    msg += *m_paperFormat;
-                break;
-
-            case 'S':
-                msg << m_sheetNumber;
-                break;
-
-            case 'N':
-                msg << m_sheetCount;
-                break;
-
-            case 'F':
-                {
-                    wxFileName fn( m_fileName );
-                    msg += fn.GetFullName();
-                }
-                break;
-
-            case 'L':
-                if( m_sheetLayer )
-                    msg += *m_sheetLayer;
-                break;
-
-            case 'P':
-                msg += m_sheetFullName;
-                break;
-
-            case 'Y':
-                if( m_titleBlock )
-                    msg += m_titleBlock->GetCompany();
-                break;
-
-            case 'T':
-                if( m_titleBlock )
-                    msg += m_titleBlock->GetTitle();
-                break;
-
-            case 'C':
-                format = aTextbase[++ii];
-                switch( format )
-                {
-                case '0':
-                case '1':
-                case '2':
-                case '3':
-                case '4':
-                case '5':
-                case '6':
-                case '7':
-                case '8':
-                case '9':
-                    if( m_titleBlock )
-                        msg += m_titleBlock->GetComment( format - '0');
-                    break;
-
-                default:
-                    break;
-                }
-
-            default:
-                break;
-        }
-    }
-
-    return msg;
+    return ExpandTextVars( aTextbase, wsResolver, m_project );
 }
 
 
diff --git a/common/page_layout/ws_proxy_view_item.cpp b/common/page_layout/ws_proxy_view_item.cpp
index 4e97c25310..0e31efaa42 100644
--- a/common/page_layout/ws_proxy_view_item.cpp
+++ b/common/page_layout/ws_proxy_view_item.cpp
@@ -1,7 +1,7 @@
 /*
  * This program source code file is part of KICAD, a free EDA CAD application.
  *
- * Copyright (C) 2013-2019 CERN
+ * Copyright (C) 2013-2020 CERN
  * @author Maciej Suminski <maciej.suminski@cern.ch>
  *
  * This program is free software; you can redistribute it and/or
@@ -25,8 +25,6 @@
 #include <ws_proxy_view_item.h>
 #include <ws_draw_item.h>
 #include <ws_data_item.h>
-#include <gal/graphics_abstraction_layer.h>
-#include <painter.h>
 #include <layers_id_colors_and_visibility.h>
 #include <page_info.h>
 #include <view/view.h>
@@ -35,30 +33,19 @@
 using namespace KIGFX;
 
 WS_PROXY_VIEW_ITEM::WS_PROXY_VIEW_ITEM( int aMils2IUscalefactor, const PAGE_INFO* aPageInfo,
-                                        const TITLE_BLOCK* aTitleBlock ) :
+                                        const PROJECT* aProject, const TITLE_BLOCK* aTitleBlock ) :
         EDA_ITEM( NOT_USED ), // this item is never added to a BOARD so it needs no type
         m_mils2IUscalefactor( aMils2IUscalefactor ),
         m_titleBlock( aTitleBlock ),
         m_pageInfo( aPageInfo ),
         m_sheetNumber( 1 ),
         m_sheetCount( 1 ),
+        m_project( aProject ),
         m_colorLayer( LAYER_WORKSHEET )
 {
 }
 
 
-void WS_PROXY_VIEW_ITEM::SetPageInfo( const PAGE_INFO* aPageInfo )
-{
-    m_pageInfo = aPageInfo;
-}
-
-
-void WS_PROXY_VIEW_ITEM::SetTitleBlock( const TITLE_BLOCK* aTitleBlock )
-{
-    m_titleBlock = aTitleBlock;
-}
-
-
 const BOX2I WS_PROXY_VIEW_ITEM::ViewBBox() const
 {
     BOX2I bbox;
@@ -95,6 +82,7 @@ void WS_PROXY_VIEW_ITEM::ViewDraw( int aLayer, VIEW* aView ) const
     drawList.SetSheetCount( m_sheetCount );
     drawList.SetFileName( fileName );
     drawList.SetSheetName( sheetName );
+    drawList.SetProject( m_project );
 
     drawList.BuildWorkSheetGraphicList( *m_pageInfo, *m_titleBlock );
 
diff --git a/common/plotters/DXF_plotter.cpp b/common/plotters/DXF_plotter.cpp
index c98610d5f4..a5e7fd3e5f 100644
--- a/common/plotters/DXF_plotter.cpp
+++ b/common/plotters/DXF_plotter.cpp
@@ -886,7 +886,7 @@ void DXF_PLOTTER::Text( const wxPoint&              aPos,
     if( ( GetTextMarkupFlags() & ENABLE_SUPERSCRIPT_MARKUP ) && aText.Contains( wxT( "^" ) ) )
         processSuperSub = true;
 
-    if( textAsLines || containsNonAsciiChars( aText ) || aMultilineAllowed || processSuperSub )
+    if( m_textAsLines || containsNonAsciiChars( aText ) || aMultilineAllowed || processSuperSub )
     {
         // output text as graphics.
         // Perhaps multiline texts could be handled as DXF text entity
diff --git a/common/plotters/common_plot_functions.cpp b/common/plotters/common_plot_functions.cpp
index c746dab203..2cbd196c17 100644
--- a/common/plotters/common_plot_functions.cpp
+++ b/common/plotters/common_plot_functions.cpp
@@ -57,9 +57,9 @@ wxString GetDefaultPlotExtension( PLOT_FORMAT aFormat )
 }
 
 
-void PlotWorkSheet( PLOTTER* plotter, const TITLE_BLOCK& aTitleBlock,
+void PlotWorkSheet( PLOTTER* plotter, const PROJECT* aProject, const TITLE_BLOCK& aTitleBlock,
                     const PAGE_INFO& aPageInfo, int aSheetNumber, int aNumberOfSheets,
-                    const wxString &aSheetDesc, const wxString &aFilename, const COLOR4D aColor )
+                    const wxString &aSheetDesc, const wxString &aFilename, COLOR4D aColor )
 {
     /* Note: Page sizes values are given in mils
      */
@@ -83,7 +83,7 @@ void PlotWorkSheet( PLOTTER* plotter, const TITLE_BLOCK& aTitleBlock,
     drawList.SetSheetCount( aNumberOfSheets );
     drawList.SetFileName( fn.GetFullName() );   // Print only the short filename
     drawList.SetSheetName( aSheetDesc );
-
+    drawList.SetProject( aProject );
 
     drawList.BuildWorkSheetGraphicList( aPageInfo, aTitleBlock );
 
diff --git a/common/project.cpp b/common/project.cpp
index 3234f1367b..b555d31a81 100644
--- a/common/project.cpp
+++ b/common/project.cpp
@@ -61,6 +61,18 @@ PROJECT::~PROJECT()
 }
 
 
+bool PROJECT::TextVarResolver( wxString* aToken ) const
+{
+    if( m_textVars.count( *aToken ) > 0 )
+    {
+        *aToken = m_textVars.at( *aToken );
+        return true;
+    }
+
+    return false;
+}
+
+
 void PROJECT::SetProjectFullName( const wxString& aFullPathAndName )
 {
     // Compare paths, rather than inodes, to be less surprising to the user.
@@ -376,6 +388,16 @@ void PROJECT::ConfigSave( const SEARCH_STACK& aSList, const wxString& aGroupName
 
     wxConfigSaveParams( cfg.get(), aParams, aGroupName );
 
+    cfg->DeleteGroup( GROUP_TEXT_VARS );
+    cfg->SetPath( GROUP_TEXT_VARS );
+    int index = 1;
+
+    for( const auto& textvar : m_textVars )
+    {
+        cfg->Write( wxString::Format( "%d", index++ ),
+                    wxString::Format( "%s:%s", textvar.first, textvar.second ) );
+    }
+
     cfg->SetPath( wxT( "/" ) );
 
     cfg->Flush();
@@ -396,7 +418,7 @@ bool PROJECT::ConfigLoad( const SEARCH_STACK& aSList, const wxString&  aGroupNam
     }
 
     // We do not want expansion of env var values when reading our project config file
-    cfg.get()->SetExpandEnvVars( false );
+    cfg->SetExpandEnvVars( false );
 
     cfg->SetPath( wxCONFIG_PATH_SEPARATOR );
 
@@ -406,6 +428,18 @@ bool PROJECT::ConfigLoad( const SEARCH_STACK& aSList, const wxString&  aGroupNam
 
     wxConfigLoadParams( cfg.get(), aParams, aGroupName );
 
+    cfg->SetPath( GROUP_TEXT_VARS );
+
+    int index = 1;
+    wxString entry;
+
+    while( cfg->Read( wxString::Format( "%d", index++ ), &entry ) )
+    {
+        wxArrayString tokens = wxSplit( entry, ':' );
+
+        if( tokens.size() == 2 )
+            m_textVars[ tokens[0] ] = tokens[1];
+    }
     return true;
 }
 
diff --git a/eeschema/template_fieldnames.cpp b/common/template_fieldnames.cpp
similarity index 100%
rename from eeschema/template_fieldnames.cpp
rename to common/template_fieldnames.cpp
diff --git a/eeschema/template_fieldnames.h b/common/template_fieldnames.h
similarity index 100%
rename from eeschema/template_fieldnames.h
rename to common/template_fieldnames.h
diff --git a/eeschema/template_fieldnames.keywords b/common/template_fieldnames.keywords
similarity index 100%
rename from eeschema/template_fieldnames.keywords
rename to common/template_fieldnames.keywords
diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt
index c9c372a700..6aad3e6df4 100644
--- a/eeschema/CMakeLists.txt
+++ b/eeschema/CMakeLists.txt
@@ -221,7 +221,6 @@ set( EESCHEMA_SRCS
     symbol_lib_table.cpp
     symbol_tree_model_adapter.cpp
     symbol_tree_synchronizing_adapter.cpp
-    template_fieldnames.cpp
     toolbars_lib_view.cpp
     toolbars_sch_editor.cpp
     transform.cpp
@@ -473,14 +472,6 @@ make_lexer(
     TLIB_T
     )
 
-make_lexer(
-    eeschema_kiface_objects
-    template_fieldnames.keywords
-    template_fieldnames_lexer.h
-    template_fieldnames_keywords.cpp
-    TFIELD_T
-    )
-
 make_lexer(
     eeschema_kiface_objects
     dialogs/dialog_bom_cfg.keywords
diff --git a/eeschema/class_libentry.cpp b/eeschema/class_libentry.cpp
index 30b8d0f040..a66947371f 100644
--- a/eeschema/class_libentry.cpp
+++ b/eeschema/class_libentry.cpp
@@ -584,15 +584,8 @@ void LIB_PART::RemoveDrawItem( LIB_ITEM* aItem )
     // omitted when saving to disk.
     if( aItem->Type() == LIB_FIELD_T )
     {
-        LIB_FIELD* field = (LIB_FIELD*) aItem;
-
-        if( field->GetId() < MANDATORY_FIELDS )
-        {
-            wxLogWarning( _(
-                "An attempt was made to remove the %s field from component %s in library %s." ),
-                field->GetName( TRANSLATE_FIELD_NAME ), GetName(), GetLibraryName() );
+        if( static_cast<LIB_FIELD*>( aItem )->GetId() < MANDATORY_FIELDS )
             return;
-        }
     }
 
     LIB_ITEMS& items = m_drawings[ aItem->Type() ];
@@ -880,7 +873,7 @@ LIB_FIELD* LIB_PART::FindField( const wxString& aFieldName )
     {
         LIB_FIELD* field = ( LIB_FIELD* ) &item;
 
-        if( field->GetName( NATIVE_FIELD_NAME ) == aFieldName )
+        if( field->GetCanonicalName() == aFieldName )
             return field;
     }
 
diff --git a/eeschema/dialogs/dialog_schematic_setup.cpp b/eeschema/dialogs/dialog_schematic_setup.cpp
index 0309c69417..42f1f5e631 100644
--- a/eeschema/dialogs/dialog_schematic_setup.cpp
+++ b/eeschema/dialogs/dialog_schematic_setup.cpp
@@ -25,13 +25,11 @@
 #include <panel_setup_pinmap.h>
 #include <eeschema_config.h>
 #include <erc_item.h>
+#include <panel_text_variables.h>
 #include "dialog_schematic_setup.h"
 #include "panel_eeschema_template_fieldnames.h"
 
 
-bool g_macHack;
-
-
 DIALOG_SCHEMATIC_SETUP::DIALOG_SCHEMATIC_SETUP( SCH_EDIT_FRAME* aFrame ) :
         PAGED_DIALOG( aFrame, _( "Schematic Setup" ),
                       _( "Import Settings from Another Project..." ) ),
@@ -45,6 +43,9 @@ DIALOG_SCHEMATIC_SETUP::DIALOG_SCHEMATIC_SETUP( SCH_EDIT_FRAME* aFrame ) :
     ERC_ITEM dummyItem;
     m_severities = new PANEL_SETUP_SEVERITIES( this, dummyItem, g_ErcSettings->m_Severities,
                                                ERCE_FIRST, ERCE_LAST );
+
+    m_textVars = new PANEL_TEXT_VARIABLES( this, &Prj() );
+
     /*
      * WARNING: If you change page names you MUST update calls to DoShowSchematicSetupDialog().
      */
@@ -57,12 +58,17 @@ DIALOG_SCHEMATIC_SETUP::DIALOG_SCHEMATIC_SETUP( SCH_EDIT_FRAME* aFrame ) :
     m_treebook->AddSubPage( m_pinMap, _( "Pin Map" ) );
     m_treebook->AddSubPage( m_severities, _( "Violation Severity" ) );
 
+    m_treebook->AddPage( new wxPanel( this ), _( "Project" ) );
+    m_treebook->AddSubPage( m_textVars, _( "Text Variables" ) );
+
 	// Connect Events
 	m_treebook->Connect( wxEVT_TREEBOOK_PAGE_CHANGED,
                          wxBookCtrlEventHandler( DIALOG_SCHEMATIC_SETUP::OnPageChange ), NULL, this );
 
     FinishDialogSettings();
-    g_macHack = true;
+
+    for( int i = 0; i < m_treebook->GetPageCount(); ++i )
+   	    m_macHack.push_back( true );
 }
 
 
@@ -76,16 +82,17 @@ DIALOG_SCHEMATIC_SETUP::~DIALOG_SCHEMATIC_SETUP()
 void DIALOG_SCHEMATIC_SETUP::OnPageChange( wxBookCtrlEvent& event )
 {
 #ifdef __WXMAC__
-    // Work around an OSX bug where the wxGrid children don't get placed correctly
-    if( g_macHack && m_treebook->GetPage( event.GetSelection() ) == m_fieldNameTemplates )
+    // Work around an OSX bug where the wxGrid children don't get placed correctly until
+    // the first resize event
+    int page = event.GetSelection();
+
+    if( m_macHack[ page ] )
     {
-        m_fieldNameTemplates->SetSize( wxSize( m_fieldNameTemplates->GetSize().x - 1,
-                                               m_fieldNameTemplates->GetSize().y ) );
+        wxSize pageSize = m_treebook->GetPage( page )->GetSize();
+        pageSize.x -= 1;
 
-        wxPoint pos = m_fieldNameTemplates->GetPosition();
-        m_fieldNameTemplates->Move( pos.x + 6, pos.y + 6 );
-
-        g_macHack = false;
+        m_treebook->GetPage( page )->SetSize( pageSize );
+        m_macHack[ page ] = false;
     }
 #endif
 }
diff --git a/eeschema/dialogs/dialog_schematic_setup.h b/eeschema/dialogs/dialog_schematic_setup.h
index 5d3acf0209..f8a962fa1c 100644
--- a/eeschema/dialogs/dialog_schematic_setup.h
+++ b/eeschema/dialogs/dialog_schematic_setup.h
@@ -28,6 +28,7 @@ class PANEL_SETUP_SEVERITIES;
 class PANEL_EESCHEMA_TEMPLATE_FIELDNAMES;
 class PANEL_SETUP_FORMATTING;
 class PANEL_SETUP_PINMAP;
+class PANEL_TEXT_VARIABLES;
 
 
 class DIALOG_SCHEMATIC_SETUP : public PAGED_DIALOG
@@ -45,6 +46,9 @@ protected:
     PANEL_EESCHEMA_TEMPLATE_FIELDNAMES* m_fieldNameTemplates;
     PANEL_SETUP_PINMAP*                 m_pinMap;
     PANEL_SETUP_SEVERITIES*             m_severities;
+    PANEL_TEXT_VARIABLES*               m_textVars;
+
+    std::vector<bool>                   m_macHack;
 
     // event handlers
     void OnPageChange( wxBookCtrlEvent& event );
diff --git a/eeschema/dialogs/dialog_spice_model.cpp b/eeschema/dialogs/dialog_spice_model.cpp
index 8faf6e84b0..451ae514c8 100644
--- a/eeschema/dialogs/dialog_spice_model.cpp
+++ b/eeschema/dialogs/dialog_spice_model.cpp
@@ -275,12 +275,23 @@ bool DIALOG_SPICE_MODEL::TransferDataFromWindow()
             const wxString& spiceField = NETLIST_EXPORTER_PSPICE::GetSpiceFieldName( (SPICE_FIELD) i );
 
             if( m_useSchFields )
+            {
                 m_schfields->erase( std::remove_if( m_schfields->begin(), m_schfields->end(),
-                        [&]( const SCH_FIELD& f )
-                            { return f.GetName() == spiceField; } ), m_schfields->end() );
+                                                    [&]( const SCH_FIELD& f )
+                                                    {
+                                                        return f.GetName() == spiceField;
+                                                    } ),
+                                    m_schfields->end() );
+            }
             else
+            {
                 m_libfields->erase( std::remove_if( m_libfields->begin(), m_libfields->end(),
-                        [&]( const LIB_FIELD& f ) { return f.GetName( NATIVE_FIELD_NAME ) == spiceField; } ), m_libfields->end() );
+                                                    [&]( const LIB_FIELD& f )
+                                                    {
+                                                        return f.GetName() == spiceField;
+                                                    } ),
+                                    m_libfields->end() );
+            }
         }
     }
 
@@ -317,7 +328,7 @@ bool DIALOG_SPICE_MODEL::TransferDataToWindow()
             // TODO: There must be a good way to template out these repetitive calls
             for( const LIB_FIELD& field : *m_libfields )
             {
-                if( field.GetName( NATIVE_FIELD_NAME ) == spiceField  && !field.GetText().IsEmpty() )
+                if( field.GetName() == spiceField  && !field.GetText().IsEmpty() )
                 {
                     m_fieldsTmp[idx] = field.GetText();
                     break;
@@ -868,9 +879,11 @@ LIB_FIELD& DIALOG_SPICE_MODEL::getLibField( int aFieldType )
 {
     const wxString& spiceField = NETLIST_EXPORTER_PSPICE::GetSpiceFieldName( (SPICE_FIELD) aFieldType );
 
-    auto fieldIt = std::find_if( m_libfields->begin(), m_libfields->end(), [&]( const LIB_FIELD& f ) {
-        return f.GetName( NATIVE_FIELD_NAME ) == spiceField;
-    } );
+    auto fieldIt = std::find_if( m_libfields->begin(), m_libfields->end(),
+                                 [&]( const LIB_FIELD& f )
+                                 {
+                                     return f.GetName() == spiceField;
+                                 } );
 
     // Found one, so return it
     if( fieldIt != m_libfields->end() )
diff --git a/eeschema/dialogs/dialog_update_fields.cpp b/eeschema/dialogs/dialog_update_fields.cpp
index da2b121a65..bcdce8b0c8 100644
--- a/eeschema/dialogs/dialog_update_fields.cpp
+++ b/eeschema/dialogs/dialog_update_fields.cpp
@@ -111,7 +111,7 @@ bool DIALOG_UPDATE_FIELDS::TransferDataToWindow()
                 const LIB_FIELD* field = static_cast<const LIB_FIELD*>( &( *it ) );
 
                 if( field->GetId() >= MANDATORY_FIELDS )
-                    m_fields.insert( field->GetName( false ) );
+                    m_fields.insert( field->GetName() );
             }
         }
     }
@@ -121,7 +121,7 @@ bool DIALOG_UPDATE_FIELDS::TransferDataToWindow()
 
     for( int i = 0; i < MANDATORY_FIELDS; ++i )
     {
-        m_fieldsBox->Append( m_components.front()->GetField( i )->GetName( false ) );
+        m_fieldsBox->Append( m_components.front()->GetField( i )->GetName() );
 
         if( i != REFERENCE && i != VALUE )
             m_fieldsBox->Check( i, true );
diff --git a/eeschema/dialogs/panel_eeschema_color_settings.cpp b/eeschema/dialogs/panel_eeschema_color_settings.cpp
index 07e01e7bb8..ccb189c5c3 100644
--- a/eeschema/dialogs/panel_eeschema_color_settings.cpp
+++ b/eeschema/dialogs/panel_eeschema_color_settings.cpp
@@ -299,7 +299,7 @@ void PANEL_EESCHEMA_COLOR_SETTINGS::createPreviewItems()
     m_page->SetHeightMils( 5000 );
     m_page->SetWidthMils( 5500 );
 
-    m_ws = new KIGFX::WS_PROXY_VIEW_ITEM( static_cast<int>( IU_PER_MILS ), m_page, m_titleBlock );
+    m_ws = new KIGFX::WS_PROXY_VIEW_ITEM( (int) IU_PER_MILS, m_page, nullptr, m_titleBlock );
     m_ws->SetColorLayer( LAYER_SCHEMATIC_WORKSHEET );
     view->Add( m_ws );
 
diff --git a/eeschema/generate_alias_info.cpp b/eeschema/generate_alias_info.cpp
index f7e792162b..c2cdc748f9 100644
--- a/eeschema/generate_alias_info.cpp
+++ b/eeschema/generate_alias_info.cpp
@@ -159,7 +159,7 @@ protected:
 
     wxString GetHtmlFieldRow( LIB_FIELD const & aField )
     {
-        wxString name = aField.GetName( NATIVE_FIELD_NAME );
+        wxString name = aField.GetCanonicalName();
         wxString text = aField.GetFullText( m_unit > 0 ? m_unit : 1 );
         wxString fieldhtml = FieldFormat;
 
@@ -225,7 +225,7 @@ protected:
 
                 for( auto const& parentField : parentFields )
                 {
-                    if( m_symbol->FindField( parentField.GetName( NATIVE_FIELD_NAME ) ) )
+                    if( m_symbol->FindField( parentField.GetCanonicalName() ) )
                         continue;
 
                     fieldtable += GetHtmlFieldRow( parentField );
diff --git a/eeschema/lib_field.cpp b/eeschema/lib_field.cpp
index eb969122a6..d39b6cd02c 100644
--- a/eeschema/lib_field.cpp
+++ b/eeschema/lib_field.cpp
@@ -384,22 +384,26 @@ COLOR4D LIB_FIELD::GetDefaultColor()
 }
 
 
-wxString LIB_FIELD::GetName( bool aTranslate ) const
+wxString LIB_FIELD::GetName( bool aUseDefaultName ) const
+{
+    if( !m_name.IsEmpty() )
+        return m_name;
+    else if( aUseDefaultName )
+        return TEMPLATE_FIELDNAME::GetDefaultFieldName( m_id );
+}
+
+
+wxString LIB_FIELD::GetCanonicalName() const
 {
     switch( m_id )
     {
-    case REFERENCE: return aTranslate ? _( "Reference" ) : wxT( "Reference" );
-    case VALUE:     return aTranslate ? _( "Value" ) : wxT( "Value" );
-    case FOOTPRINT: return aTranslate ? _( "Footprint" ) : wxT( "Footprint" );
-    case DATASHEET: return aTranslate ? _( "Datasheet" ) : wxT( "Datasheet" );
-
-    default:
-        if( m_name.IsEmpty() )
-            return aTranslate ? wxString::Format( _( "Field%d" ), m_id )
-                              : wxString::Format( wxT( "Field%d" ), m_id );
-        else
-            return m_name;
+    case  REFERENCE: return wxT( "Reference" );
+    case  VALUE:     return wxT( "Value" );
+    case  FOOTPRINT: return wxT( "Footprint" );
+    case  DATASHEET: return wxT( "Datasheet" );
     }
+
+    return m_name;
 }
 
 
@@ -425,7 +429,8 @@ void LIB_FIELD::SetName( const wxString& aName )
 
 wxString LIB_FIELD::GetSelectMenuText( EDA_UNITS aUnits ) const
 {
-    return wxString::Format( _( "Field %s \"%s\"" ), GetName( TRANSLATE_FIELD_NAME ),
+    return wxString::Format( _( "Field %s \"%s\"" ),
+                             GetName(),
                              ShortenedShownText() );
 }
 
@@ -459,7 +464,7 @@ void LIB_FIELD::GetMsgPanelInfo( EDA_UNITS aUnits, MSG_PANEL_ITEMS& aList )
     aList.push_back( MSG_PANEL_ITEM( _( "Height" ), msg, BLUE ) );
 
     // Display field name (ref, value ...)
-    aList.push_back( MSG_PANEL_ITEM( _( "Field" ), GetName( TRANSLATE_FIELD_NAME ), BROWN ) );
+    aList.push_back( MSG_PANEL_ITEM( _( "Field" ), GetName(), BROWN ) );
 
     // Display field text:
     aList.push_back( MSG_PANEL_ITEM( _( "Value" ), GetShownText(), BROWN ) );
diff --git a/eeschema/lib_field.h b/eeschema/lib_field.h
index 0ae3f81f6f..d2be03f97a 100644
--- a/eeschema/lib_field.h
+++ b/eeschema/lib_field.h
@@ -111,19 +111,18 @@ public:
      * Returns the field name.
      *
      * The first four field IDs are reserved and therefore always return their respective
-     * names.  The user definable fields will return FieldN where N is the ID of the field
-     * when the m_name member is empty.
+     * names.
      *
-     * @param aTranslate true to return translated field name.
-     * note: has meaning mainly for mandatory fields or to return a default field name.
-     * Should be used only in messages (never when trying to find a field by name)
-     * false to return the english name.
-     * Normal option when the name is used as keyword in netlists.
-     * @return Name of the field.
+     * The user definable fields will return FieldN where N is the ID of the field when the
+     * m_name member is empyt unless false is passed to \a aUseDefaultName.
      */
-    #define TRANSLATE_FIELD_NAME true
-    #define NATIVE_FIELD_NAME false
-    wxString GetName( bool aTranslate ) const;
+    wxString GetName( bool aUseDefaultName = true ) const;
+
+    /**
+     * Get a non-language-specific name for a field which can be used for storage, variable
+     * look-up, etc.
+     */
+    wxString GetCanonicalName() const;
 
     /**
      * Set a user definable field name to \a aName.
diff --git a/eeschema/netlist_exporters/netlist_exporter_generic.cpp b/eeschema/netlist_exporters/netlist_exporter_generic.cpp
index 7d984df2bb..d9d523edde 100644
--- a/eeschema/netlist_exporters/netlist_exporter_generic.cpp
+++ b/eeschema/netlist_exporters/netlist_exporter_generic.cpp
@@ -448,7 +448,7 @@ XNODE* NETLIST_EXPORTER_GENERIC::makeLibParts()
             {
                 XNODE*     xfield;
                 xfields->AddChild( xfield = node( "field", fieldList[i].GetText() ) );
-                xfield->AddAttribute( "name", fieldList[i].GetName(false) );
+                xfield->AddAttribute( "name", fieldList[i].GetCanonicalName() );
             }
         }
 
diff --git a/eeschema/plot_schematic_DXF.cpp b/eeschema/plot_schematic_DXF.cpp
index 45b0f5d5a0..09aa0077c6 100644
--- a/eeschema/plot_schematic_DXF.cpp
+++ b/eeschema/plot_schematic_DXF.cpp
@@ -104,11 +104,11 @@ void DIALOG_PLOT_SCHEMATIC::CreateDXFFile( bool aPlotAll, bool aPlotFrameRef )
 }
 
 
-bool DIALOG_PLOT_SCHEMATIC::PlotOneSheetDXF( const wxString&    aFileName,
-                                             SCH_SCREEN*        aScreen,
-                                             wxPoint            aPlotOffset,
-                                             double             aScale,
-                                             bool aPlotFrameRef )
+bool DIALOG_PLOT_SCHEMATIC::PlotOneSheetDXF( const wxString& aFileName,
+                                             SCH_SCREEN*     aScreen,
+                                             wxPoint         aPlotOffset,
+                                             double          aScale,
+                                             bool            aPlotFrameRef )
 {
     DXF_PLOTTER* plotter = new DXF_PLOTTER();
 
@@ -137,16 +137,10 @@ bool DIALOG_PLOT_SCHEMATIC::PlotOneSheetDXF( const wxString&    aFileName,
 
     if( aPlotFrameRef )
     {
-        COLOR4D color = plotter->GetColorMode() ?
-                        plotter->ColorSettings()->GetColor( LAYER_SCHEMATIC_WORKSHEET ) :
-                        COLOR4D::BLACK;
-
-        PlotWorkSheet( plotter, m_parent->GetTitleBlock(),
-                       m_parent->GetPageSettings(),
+        PlotWorkSheet( plotter, &m_parent->Prj(), m_parent->GetTitleBlock(), pageInfo,
                        aScreen->m_ScreenNumber, aScreen->m_NumberOfScreens,
-                       m_parent->GetScreenDesc(),
-                       aScreen->GetFileName(),
-                       color );
+                       m_parent->GetScreenDesc(), aScreen->GetFileName(),
+                       plotter->GetColorMode() ? COLOR4D::UNSPECIFIED : COLOR4D::BLACK );
     }
 
     aScreen->Plot( plotter );
diff --git a/eeschema/plot_schematic_HPGL.cpp b/eeschema/plot_schematic_HPGL.cpp
index 7897f81905..0ddf601524 100644
--- a/eeschema/plot_schematic_HPGL.cpp
+++ b/eeschema/plot_schematic_HPGL.cpp
@@ -202,13 +202,12 @@ bool DIALOG_PLOT_SCHEMATIC::Plot_1_Page_HPGL( const wxString&   aFileName,
     plotter->SetPenDiameter( m_HPGLPenSize );
     plotter->StartPlot();
 
-    if( getPlotFrameRef() )
-        PlotWorkSheet( plotter, m_parent->GetTitleBlock(),
-                       m_parent->GetPageSettings(),
+    if( aPlotFrameRef )
+    {
+        PlotWorkSheet( plotter, &m_parent->Prj(), m_parent->GetTitleBlock(), aPageInfo,
                        aScreen->m_ScreenNumber, aScreen->m_NumberOfScreens,
-                       m_parent->GetScreenDesc(),
-                       aScreen->GetFileName(),
-                       COLOR4D::BLACK );
+                       m_parent->GetScreenDesc(), aScreen->GetFileName(), COLOR4D::BLACK );
+    }
 
     aScreen->Plot( plotter );
 
diff --git a/eeschema/plot_schematic_PDF.cpp b/eeschema/plot_schematic_PDF.cpp
index 92f95af92a..15039f7452 100644
--- a/eeschema/plot_schematic_PDF.cpp
+++ b/eeschema/plot_schematic_PDF.cpp
@@ -161,16 +161,11 @@ void DIALOG_PLOT_SCHEMATIC::plotOneSheetPDF( PLOTTER* aPlotter,
 
     if( aPlotFrameRef )
     {
-        COLOR4D color = aPlotter->GetColorMode() ?
-                                aPlotter->ColorSettings()->GetColor( LAYER_SCHEMATIC_WORKSHEET ) :
-                                COLOR4D::BLACK;
-
-        PlotWorkSheet( aPlotter, m_parent->GetTitleBlock(),
-                       m_parent->GetPageSettings(),
-                       aScreen->m_ScreenNumber, aScreen->m_NumberOfScreens,
-                       m_parent->GetScreenDesc(),
+        PlotWorkSheet( aPlotter, &aScreen->Prj(), m_parent->GetTitleBlock(),
+                       m_parent->GetPageSettings(), aScreen->m_ScreenNumber,
+                       aScreen->m_NumberOfScreens, m_parent->GetScreenDesc(),
                        aScreen->GetFileName(),
-                       color );
+                       aPlotter->GetColorMode() ? COLOR4D::UNSPECIFIED : COLOR4D::BLACK );
     }
 
     aScreen->Plot( aPlotter );
diff --git a/eeschema/plot_schematic_PS.cpp b/eeschema/plot_schematic_PS.cpp
index 27e0925384..8c0ed02395 100644
--- a/eeschema/plot_schematic_PS.cpp
+++ b/eeschema/plot_schematic_PS.cpp
@@ -170,16 +170,10 @@ bool DIALOG_PLOT_SCHEMATIC::plotOneSheetPS( const wxString&     aFileName,
 
     if( aPlotFrameRef )
     {
-        COLOR4D color = plotter->GetColorMode() ?
-                        plotter->ColorSettings()->GetColor( LAYER_SCHEMATIC_WORKSHEET ) :
-                        COLOR4D::BLACK;
-
-        PlotWorkSheet( plotter, m_parent->GetTitleBlock(),
-                       m_parent->GetPageSettings(),
+        PlotWorkSheet( plotter, &aScreen->Prj(), m_parent->GetTitleBlock(), aPageInfo,
                        aScreen->m_ScreenNumber, aScreen->m_NumberOfScreens,
-                       m_parent->GetScreenDesc(),
-                       aScreen->GetFileName(),
-                       color );
+                       m_parent->GetScreenDesc(), aScreen->GetFileName(),
+                       plotter->GetColorMode() ? COLOR4D::UNSPECIFIED : COLOR4D::BLACK );
     }
 
     aScreen->Plot( plotter );
diff --git a/eeschema/plot_schematic_SVG.cpp b/eeschema/plot_schematic_SVG.cpp
index 2b3d0a0fcc..ea5bf3bcff 100644
--- a/eeschema/plot_schematic_SVG.cpp
+++ b/eeschema/plot_schematic_SVG.cpp
@@ -148,16 +148,10 @@ bool DIALOG_PLOT_SCHEMATIC::plotOneSheetSVG( EDA_DRAW_FRAME*    aFrame,
 
     if( aPlotFrameRef )
     {
-        COLOR4D color = plotter->GetColorMode() ?
-                        plotter->ColorSettings()->GetColor( LAYER_SCHEMATIC_WORKSHEET ) :
-                        COLOR4D::BLACK;
-
-        PlotWorkSheet( plotter, aFrame->GetTitleBlock(),
-                       aFrame->GetPageSettings(),
+        PlotWorkSheet( plotter, &aScreen->Prj(), aFrame->GetTitleBlock(), pageInfo,
                        aScreen->m_ScreenNumber, aScreen->m_NumberOfScreens,
-                       aFrame->GetScreenDesc(),
-                       aScreen->GetFileName(),
-                       color );
+                       aFrame->GetScreenDesc(), aScreen->GetFileName(),
+                       plotter->GetColorMode() ? COLOR4D::UNSPECIFIED : COLOR4D::BLACK );
     }
 
     aScreen->Plot( plotter );
diff --git a/eeschema/sch_component.cpp b/eeschema/sch_component.cpp
index d941e6c7be..e0e078bde4 100644
--- a/eeschema/sch_component.cpp
+++ b/eeschema/sch_component.cpp
@@ -760,18 +760,6 @@ void SCH_COMPONENT::GetFields( std::vector<SCH_FIELD*>& aVector, bool aVisibleOn
 }
 
 
-std::vector<SCH_FIELD*> SCH_COMPONENT::GetFields()
-{
-    std::vector<SCH_FIELD*> retvec;
-    retvec.reserve( m_Fields.size() );
-
-    for( SCH_FIELD& field : m_Fields )
-        retvec.push_back( &field );
-
-    return retvec;
-}
-
-
 SCH_FIELD* SCH_COMPONENT::AddField( const SCH_FIELD& aField )
 {
     int newNdx = m_Fields.size();
@@ -816,19 +804,9 @@ void SCH_COMPONENT::UpdateFields( bool aResetStyle, bool aResetRef )
         LIB_FIELDS fields;
         m_part->GetFields( fields );
 
-        for( const LIB_FIELD& field : fields )
+        for( const LIB_FIELD& libField : fields )
         {
-            // Can no longer insert an empty name, since names are now keys.  The
-            // field index is not used beyond the first MANDATORY_FIELDS
-            if( field.GetName( NATIVE_FIELD_NAME ).IsEmpty() )
-                continue;
-
-            // See if field already exists (mandatory fields always exist).
-            // for mandatory fields, the name and field id are fixed, so we use the
-            // known and fixed id to get them (more reliable than names, which can be translated)
-            // for other fields (custom fields), locate the field by same name
-            // (field id has no known meaning for custom fields)
-            int idx = field.GetId();
+            int idx = libField.GetId();
             SCH_FIELD* schField;
 
             if( idx == REFERENCE && !aResetRef )
@@ -837,19 +815,21 @@ void SCH_COMPONENT::UpdateFields( bool aResetStyle, bool aResetRef )
             if( (unsigned) idx < MANDATORY_FIELDS )
                 schField = GetField( idx );
             else
-                schField = FindField( field.GetName( NATIVE_FIELD_NAME ) );
-
-            if( !schField )
             {
-                SCH_FIELD newField( wxPoint( 0, 0 ), GetFieldCount(), this,
-                                    field.GetName( NATIVE_FIELD_NAME ) );
-                schField = AddField( newField );
+                schField = FindField( libField.GetCanonicalName() );
+
+                if( !schField )
+                {
+                    wxString  fieldName = libField.GetCanonicalName();
+                    SCH_FIELD newField( wxPoint( 0, 0), GetFieldCount(), this, fieldName );
+                    schField = AddField( newField );
+                }
             }
 
             if( aResetStyle )
             {
-                schField->ImportValues( field );
-                schField->SetTextPos( m_Pos + field.GetTextPos() );
+                schField->ImportValues( libField );
+                schField->SetTextPos( m_Pos + libField.GetTextPos() );
             }
 
             if( idx == VALUE )
@@ -869,7 +849,7 @@ void SCH_COMPONENT::UpdateFields( bool aResetStyle, bool aResetRef )
             }
             else
             {
-                schField->SetText( field.GetText() );
+                schField->SetText( libField.GetText() );
             }
         }
     }
diff --git a/eeschema/sch_component.h b/eeschema/sch_component.h
index 7558c425fa..59602a94c8 100644
--- a/eeschema/sch_component.h
+++ b/eeschema/sch_component.h
@@ -372,7 +372,7 @@ public:
     /**
      * Returns a vector of fields from the component
      */
-    std::vector<SCH_FIELD*> GetFields();
+    std::vector<SCH_FIELD>& GetFields() { return m_Fields; }
 
     /**
      * Add a field to the symbol.
diff --git a/eeschema/sch_field.cpp b/eeschema/sch_field.cpp
index b0aaf7cae6..337ad054c3 100644
--- a/eeschema/sch_field.cpp
+++ b/eeschema/sch_field.cpp
@@ -2,7 +2,7 @@
  * This program source code file is part of KiCad, a free EDA CAD application.
  *
  * Copyright (C) 2015 Jean-Pierre Charras, jp.charras at wanadoo.fr
- * Copyright (C) 2004-2019 KiCad Developers, see AUTHORS.txt for contributors.
+ * Copyright (C) 2004-2020 KiCad Developers, see AUTHORS.txt for contributors.
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -31,7 +31,6 @@
  */
 
 #include <fctsys.h>
-#include <sch_draw_panel.h>
 #include <base_struct.h>
 #include <gr_basic.h>
 #include <gr_text.h>
@@ -39,7 +38,7 @@
 #include <sch_edit_frame.h>
 #include <plotter.h>
 #include <bitmaps.h>
-
+#include <kiway.h>
 #include <general.h>
 #include <class_library.h>
 #include <sch_component.h>
@@ -73,11 +72,117 @@ EDA_ITEM* SCH_FIELD::Clone() const
 }
 
 
-const wxString SCH_FIELD::GetFullyQualifiedText() const
+wxString SCH_FIELD::GetShownText() const
 {
-    wxString text = GetText();
+    auto symbolResolver = [ this ]( wxString* token ) -> bool
+                          {
+                              SCH_COMPONENT* component = static_cast<SCH_COMPONENT*>( m_Parent );
+                              std::vector<SCH_FIELD>& fields = component->GetFields();
 
-    // Note that the IDs of FIELDS and SHEETS overlap, so one must check *both* the
+                              for( int i = 0; i < MANDATORY_FIELDS; ++i )
+                              {
+                                  if( token->IsSameAs( fields[i].GetCanonicalName().Upper() ) )
+                                  {
+                                      // silently drop recursive references
+                                      if( &fields[i] == this )
+                                          *token = wxEmptyString;
+                                      else
+                                          *token = fields[i].GetShownText();
+
+                                      return true;
+                                  }
+                              }
+
+                              for( int i = MANDATORY_FIELDS; i < fields.size(); ++i )
+                              {
+                                  if( token->IsSameAs( fields[i].GetName() )
+                                        || token->IsSameAs( fields[i].GetName().Upper() ) )
+                                  {
+                                      // silently drop recursive references
+                                      if( &fields[i] == this )
+                                          *token = wxEmptyString;
+                                      else
+                                          *token = fields[i].GetShownText();
+
+                                      return true;
+                                  }
+                              }
+
+                              if( token->IsSameAs( wxT( "FOOTPRINT_LIBRARY" ) )  )
+                              {
+                                  SCH_FIELD& f = component->GetFields()[ FOOTPRINT ];
+                                  wxArrayString parts = wxSplit( f.GetText(), ':' );
+
+                                  *token = parts[0];
+                                  return true;
+                              }
+                              else if( token->IsSameAs( wxT( "FOOTPRINT_NAME" ) ) )
+                              {
+                                  SCH_FIELD& f = component->GetFields()[ FOOTPRINT ];
+                                  wxArrayString parts = wxSplit( f.GetText(), ':' );
+
+                                  *token = parts[ std::min( 1, (int) parts.size() - 1 ) ];
+                                  return true;
+                              }
+                              else if( token->IsSameAs( wxT( "UNIT" ) ) )
+                              {
+                                  *token = LIB_PART::SubReference( component->GetUnit() );
+                                  return true;
+                              }
+
+                              return false;
+                          };
+
+    auto sheetResolver = [ & ]( wxString* token ) -> bool
+                         {
+                             SCH_SHEET* sheet = static_cast<SCH_SHEET*>( m_Parent );
+                             std::vector<SCH_FIELD>& fields = sheet->GetFields();
+
+                             for( int i = 0; i < SHEET_MANDATORY_FIELDS; ++i )
+                             {
+                                 if( token->IsSameAs( fields[i].GetCanonicalName().Upper() ) )
+                                 {
+                                     // silently drop recursive references
+                                     if( &fields[i] == this )
+                                         *token = wxEmptyString;
+                                     else
+                                         *token = fields[i].GetShownText();
+
+                                     return true;
+                                 }
+                             }
+
+                             for( int i = SHEET_MANDATORY_FIELDS; i < fields.size(); ++i )
+                             {
+                                 if( token->IsSameAs( fields[i].GetName() ) )
+                                 {
+                                     // silently drop recursive references
+                                     if( &fields[i] == this )
+                                         *token = wxEmptyString;
+                                     else
+                                         *token = fields[i].GetShownText();
+
+                                     return true;
+                                 }
+                             }
+
+                             return false;
+                         };
+
+    PROJECT*  project = nullptr;
+    wxString  text;
+
+    if( g_RootSheet && g_RootSheet->GetScreen() )
+        project = &g_RootSheet->GetScreen()->Kiway().Prj();
+
+    if( m_Parent && m_Parent->Type() == SCH_COMPONENT_T )
+        text = ExpandTextVars( GetText(), symbolResolver, project );
+    else if( m_Parent && m_Parent->Type() == SCH_SHEET_T )
+        text = ExpandTextVars( GetText(), sheetResolver, project );
+    else
+        text = GetText();
+
+    // WARNING: the IDs of FIELDS and SHEETS overlap, so one must check *both* the
     // id and the parent's type.
 
     if( m_Parent && m_Parent->Type() == SCH_COMPONENT_T )
@@ -177,7 +282,7 @@ void SCH_FIELD::Print( wxDC* aDC, const wxPoint& aOffset )
     else
         color = GetLayerColor( m_Layer );
 
-    GRText( aDC, textpos, color, GetFullyQualifiedText(), orient, GetTextSize(),
+    GRText( aDC, textpos, color, GetShownText(), orient, GetTextSize(),
             GR_TEXT_HJUSTIFY_CENTER, GR_TEXT_VJUSTIFY_CENTER, lineWidth, IsItalic(), IsBold() );
 }
 
@@ -213,7 +318,7 @@ const EDA_RECT SCH_FIELD::GetBoundingBox() const
     EDA_RECT  rect;
     SCH_FIELD text( *this );    // Make a local copy to change text
                                 // because GetBoundingBox() is const
-    text.SetText( GetFullyQualifiedText() );
+    text.SetText( GetShownText() );
     rect = text.GetTextBox( -1, linewidth, false, GetTextMarkupFlags() );
 
     // Calculate the bounding box position relative to the parent:
@@ -278,7 +383,7 @@ bool SCH_FIELD::IsVoid() const
 
 bool SCH_FIELD::Matches( wxFindReplaceData& aSearchData, void* aAuxData )
 {
-    wxString text = GetFullyQualifiedText();
+    wxString text = GetShownText();
     int      flags = aSearchData.GetFlags();
     bool     searchUserDefinedFields = flags & FR_SEARCH_ALL_FIELDS;
     bool     searchAndReplace = flags & FR_SEARCH_REPLACE;
@@ -416,6 +521,31 @@ wxString SCH_FIELD::GetName( bool aUseDefaultName ) const
 }
 
 
+wxString SCH_FIELD::GetCanonicalName() const
+{
+    if( m_Parent && m_Parent->Type() == SCH_COMPONENT_T )
+    {
+        switch( m_id )
+        {
+        case  REFERENCE: return wxT( "Reference" );
+        case  VALUE:     return wxT( "Value" );
+        case  FOOTPRINT: return wxT( "Footprint" );
+        case  DATASHEET: return wxT( "Datasheet" );
+        }
+    }
+    else if( m_Parent && m_Parent->Type() == SCH_SHEET_T )
+    {
+        switch( m_id )
+        {
+        case  SHEETNAME:     return wxT( "Sheetname" );
+        case  SHEETFILENAME: return wxT( "Sheetfile" );
+        }
+    }
+
+    return m_name;
+}
+
+
 BITMAP_DEF SCH_FIELD::GetMenuImage() const
 {
     if( m_Parent && m_Parent->Type() == SCH_COMPONENT_T )
@@ -474,8 +604,7 @@ void SCH_FIELD::Plot( PLOTTER* aPlotter )
     if( IsVoid() )
         return;
 
-    /* Calculate the text orientation, according to the component
-     * orientation/mirror */
+    // Calculate the text orientation, according to the component orientation/mirror
     int orient = GetTextAngle();
 
     if( m_Parent && m_Parent->Type() == SCH_COMPONENT_T )
@@ -491,16 +620,16 @@ void SCH_FIELD::Plot( PLOTTER* aPlotter )
         }
     }
 
-    /* Calculate the text justification, according to the component
-     * orientation/mirror
+    /*
+     * Calculate the text justification, according to the component orientation/mirror
      * this is a bit complicated due to cumulative calculations:
      * - numerous cases (mirrored or not, rotation)
-     * - the DrawGraphicText function recalculate also H and H justifications
-     *      according to the text orientation.
-     * - When a component is mirrored, the text is not mirrored and
-     *   justifications are complicated to calculate
-     * so the more easily way is to use no justifications ( Centered text )
-     * and use GetBoundaryBox to know the text coordinate considered as centered
+     * - the DrawGraphicText function also recalculates H and H justifications according to the
+     *   text orientation.
+     * - When a component is mirrored, the text is not mirrored and justifications are
+     *   complicated to calculate
+     * so the easier way is to use no justifications (centered text) and use GetBoundaryBox to
+     * know the text coordinate considered as centered
      */
     EDA_RECT BoundaryBox = GetBoundingBox();
     EDA_TEXT_HJUSTIFY_T hjustify = GR_TEXT_HJUSTIFY_CENTER;
@@ -509,9 +638,8 @@ void SCH_FIELD::Plot( PLOTTER* aPlotter )
 
     int      thickness = GetPenSize();
 
-    aPlotter->Text( textpos, color, GetFullyQualifiedText(), orient, GetTextSize(),
-            hjustify, vjustify,
-            thickness, IsItalic(), IsBold() );
+    aPlotter->Text( textpos, color, GetShownText(), orient, GetTextSize(),  hjustify, vjustify,
+                    thickness, IsItalic(), IsBold() );
 }
 
 
diff --git a/eeschema/sch_field.h b/eeschema/sch_field.h
index 5062db44e8..f747a78d56 100644
--- a/eeschema/sch_field.h
+++ b/eeschema/sch_field.h
@@ -103,21 +103,19 @@ public:
      */
     wxString GetName( bool aUseDefaultName = true ) const;
 
+    /**
+     * Get a non-language-specific name for a field which can be used for storage, variable
+     * look-up, etc.
+     */
+    wxString GetCanonicalName() const;
+
     void SetName( const wxString& aName ) { m_name = aName; }
 
     int GetId() const { return m_id; }
 
     void SetId( int aId ) { m_id = aId; }
 
-    /**
-     * Function GetFullyQualifiedText
-     * returns the fully qualified field text by allowing for the part suffix to be added
-     * to the reference designator field if the component has multiple parts.  For all other
-     * fields this is the equivalent of EDA_TEXT::GetText().
-     *
-     * @return a const wxString object containing the field's string.
-     */
-    const wxString GetFullyQualifiedText() const;
+    wxString GetShownText() const override;
 
     const EDA_RECT GetBoundingBox() const override;
 
diff --git a/eeschema/sch_item.cpp b/eeschema/sch_item.cpp
index a4143a762d..090275007c 100644
--- a/eeschema/sch_item.cpp
+++ b/eeschema/sch_item.cpp
@@ -92,8 +92,8 @@ SCH_ITEM* SCH_ITEM::Duplicate( bool doClone ) const
         for( SCH_PIN* pin : component->GetSchPins() )
             pin->ClearFlags( SELECTED | HIGHLIGHTED | BRIGHTENED );
 
-        for( SCH_FIELD* field : component->GetFields() )
-            field->ClearFlags( SELECTED | HIGHLIGHTED | BRIGHTENED );
+        for( SCH_FIELD& field : component->GetFields() )
+            field.ClearFlags( SELECTED | HIGHLIGHTED | BRIGHTENED );
     }
 
     if( newItem->Type() == SCH_SHEET_T )
diff --git a/eeschema/sch_legacy_plugin.cpp b/eeschema/sch_legacy_plugin.cpp
index 1836221258..dc855e4261 100644
--- a/eeschema/sch_legacy_plugin.cpp
+++ b/eeschema/sch_legacy_plugin.cpp
@@ -2506,7 +2506,7 @@ LIB_PART* SCH_LEGACY_PLUGIN_CACHE::removeSymbol( LIB_PART* aPart )
                 {
                     LIB_FIELD& field = static_cast<LIB_FIELD&>( drawItem );
 
-                    if( firstChild->FindField( field.GetName( NATIVE_FIELD_NAME ) ) )
+                    if( firstChild->FindField( field.GetCanonicalName() ) )
                         continue;
                 }
 
diff --git a/eeschema/sch_painter.cpp b/eeschema/sch_painter.cpp
index b97ccf4bdb..e265025608 100644
--- a/eeschema/sch_painter.cpp
+++ b/eeschema/sch_painter.cpp
@@ -1353,8 +1353,8 @@ void SCH_PAINTER::draw( SCH_COMPONENT *aComp, int aLayer )
     draw( &tempPart, aLayer, false, aComp->GetUnit(), aComp->GetConvert() );
 
     // The fields are SCH_COMPONENT-specific so don't need to be copied/oriented/translated
-    for( SCH_FIELD* field : aComp->GetFields() )
-        draw( field, aLayer );
+    for( SCH_FIELD& field : aComp->GetFields() )
+        draw( &field, aLayer );
 }
 
 
@@ -1435,8 +1435,7 @@ void SCH_PAINTER::draw( SCH_FIELD *aField, int aLayer )
         m_gal->SetFontItalic( aField->IsItalic() );
         m_gal->SetTextMirrored( aField->IsMirrored() );
 
-        strokeText( aField->GetFullyQualifiedText(), textpos,
-                    orient == TEXT_ANGLE_VERT ? M_PI / 2 : 0 );
+        strokeText( aField->GetShownText(), textpos, orient == TEXT_ANGLE_VERT ? M_PI / 2 : 0 );
     }
 
     // Draw the umbilical line
diff --git a/eeschema/sch_sexpr_plugin.cpp b/eeschema/sch_sexpr_plugin.cpp
index 2749cc5eba..feaaf52d94 100644
--- a/eeschema/sch_sexpr_plugin.cpp
+++ b/eeschema/sch_sexpr_plugin.cpp
@@ -890,9 +890,9 @@ void SCH_SEXPR_PLUGIN::saveSymbol( SCH_COMPONENT* aSymbol, int aNestLevel )
     m_out->Print( aNestLevel + 1, "(uuid %s)\n",
                   m_out->Quotew( aSymbol->m_Uuid.AsString() ).c_str() );
 
-    for( auto field : aSymbol->GetFields() )
+    for( SCH_FIELD& field : aSymbol->GetFields() )
     {
-        saveField( field, aNestLevel + 1 );
+        saveField( &field, aNestLevel + 1 );
     }
 
     // @todo Save sheet UUID at top level of schematic file.  This will require saving from
@@ -1304,7 +1304,7 @@ LIB_PART* SCH_SEXPR_PLUGIN_CACHE::removeSymbol( LIB_PART* aPart )
                 {
                     LIB_FIELD& field = static_cast<LIB_FIELD&>( drawItem );
 
-                    if( firstChild->FindField( field.GetName( NATIVE_FIELD_NAME ) ) )
+                    if( firstChild->FindField( field.GetCanonicalName() ) )
                         continue;
                 }
 
@@ -1797,7 +1797,7 @@ void SCH_SEXPR_PLUGIN_CACHE::saveField( LIB_FIELD* aField,
 {
     wxCHECK_RET( aField && aField->Type() == LIB_FIELD_T, "Invalid LIB_FIELD object." );
 
-    wxString fieldName = aField->GetName( NATIVE_FIELD_NAME );
+    wxString fieldName = aField->GetCanonicalName();
 
     // When saving legacy fields, prefix the field name with "ki_" to prevent name clashes
     // with exisiting user defined fields.
diff --git a/eeschema/sch_sheet_path.cpp b/eeschema/sch_sheet_path.cpp
index 3befe54961..19dcf97829 100644
--- a/eeschema/sch_sheet_path.cpp
+++ b/eeschema/sch_sheet_path.cpp
@@ -423,12 +423,12 @@ SCH_ITEM* SCH_SHEET_LIST::GetItem( const KIID& aID, SCH_SHEET_PATH* aPathOut )
             {
                 SCH_COMPONENT* comp = static_cast<SCH_COMPONENT*>( aItem );
 
-                for( SCH_FIELD* field : comp->GetFields() )
+                for( SCH_FIELD& field : comp->GetFields() )
                 {
-                    if( field->m_Uuid == aID )
+                    if( field.m_Uuid == aID )
                     {
                         *aPathOut = sheet;
-                        return field;
+                        return &field;
                     }
                 }
 
@@ -445,6 +445,15 @@ SCH_ITEM* SCH_SHEET_LIST::GetItem( const KIID& aID, SCH_SHEET_PATH* aPathOut )
             {
                 SCH_SHEET* sch_sheet = static_cast<SCH_SHEET*>( aItem );
 
+                for( SCH_FIELD& field : sch_sheet->GetFields() )
+                {
+                    if( field.m_Uuid == aID )
+                    {
+                        *aPathOut = sheet;
+                        return &field;
+                    }
+                }
+
                 for( SCH_SHEET_PIN* pin : sch_sheet->GetPins() )
                 {
                     if( pin->m_Uuid == aID )
diff --git a/eeschema/sch_view.cpp b/eeschema/sch_view.cpp
index 0734aadbbc..725cb8d591 100644
--- a/eeschema/sch_view.cpp
+++ b/eeschema/sch_view.cpp
@@ -97,6 +97,7 @@ void SCH_VIEW::DisplaySheet( SCH_SCREEN *aScreen )
 
     m_worksheet.reset( new KIGFX::WS_PROXY_VIEW_ITEM( static_cast< int >( IU_PER_MILS ),
                                                       &aScreen->GetPageSettings(),
+                                                      &aScreen->Prj(),
                                                       &aScreen->GetTitleBlock() ) );
     m_worksheet->SetSheetNumber( aScreen->m_ScreenNumber );
     m_worksheet->SetSheetCount( aScreen->m_NumberOfScreens );
diff --git a/eeschema/tools/ee_selection_tool.cpp b/eeschema/tools/ee_selection_tool.cpp
index 926f493d0a..e538a53b5c 100644
--- a/eeschema/tools/ee_selection_tool.cpp
+++ b/eeschema/tools/ee_selection_tool.cpp
@@ -989,10 +989,10 @@ void EE_SELECTION_TOOL::RebuildSelection()
             {
                 if( item->Type() == SCH_COMPONENT_T )
                 {
-                    for( SCH_FIELD* field : static_cast<SCH_COMPONENT*>( item )->GetFields() )
+                    for( SCH_FIELD& field : static_cast<SCH_COMPONENT*>( item )->GetFields() )
                     {
-                        if( field->IsSelected() )
-                            select( field );
+                        if( field.IsSelected() )
+                            select( &field );
                     }
                 }
 
@@ -1231,12 +1231,12 @@ void EE_SELECTION_TOOL::highlight( EDA_ITEM* aItem, int aMode, EE_SELECTION* aGr
                 pin->SetBrightened();
         }
 
-        for( SCH_FIELD* field : static_cast<SCH_COMPONENT*>( aItem )->GetFields() )
+        for( SCH_FIELD& field : static_cast<SCH_COMPONENT*>( aItem )->GetFields() )
         {
             if( aMode == SELECTED )
-                field->SetSelected();
+                field.SetSelected();
             else if( aMode == BRIGHTENED )
-                field->SetBrightened();
+                field.SetBrightened();
         }
     }
     else if( itemType == SCH_SHEET_T )
@@ -1291,12 +1291,12 @@ void EE_SELECTION_TOOL::unhighlight( EDA_ITEM* aItem, int aMode, EE_SELECTION* a
                 pin->ClearBrightened();
         }
 
-        for( SCH_FIELD* field : static_cast<SCH_COMPONENT*>( aItem )->GetFields() )
+        for( SCH_FIELD& field : static_cast<SCH_COMPONENT*>( aItem )->GetFields() )
         {
             if( aMode == SELECTED )
-                field->ClearSelected();
+                field.ClearSelected();
             else if( aMode == BRIGHTENED )
-                field->ClearBrightened();
+                field.ClearBrightened();
         }
     }
     else if( itemType == SCH_SHEET_T )
diff --git a/eeschema/tools/lib_edit_tool.cpp b/eeschema/tools/lib_edit_tool.cpp
index f88ed33163..e4b56108c9 100644
--- a/eeschema/tools/lib_edit_tool.cpp
+++ b/eeschema/tools/lib_edit_tool.cpp
@@ -482,7 +482,7 @@ void LIB_EDIT_TOOL::editFieldProperties( LIB_FIELD* aField )
     if( aField->GetId() == VALUE )
         caption = _( "Edit Component Name" );
     else
-        caption.Printf( _( "Edit %s Field" ), aField->GetName( TRANSLATE_FIELD_NAME ) );
+        caption.Printf( _( "Edit %s Field" ), aField->GetName() );
 
     DIALOG_LIB_EDIT_ONE_FIELD dlg( m_frame, caption, aField );
 
diff --git a/eeschema/tools/sch_editor_control.cpp b/eeschema/tools/sch_editor_control.cpp
index 89c5b38402..c8df41bfea 100644
--- a/eeschema/tools/sch_editor_control.cpp
+++ b/eeschema/tools/sch_editor_control.cpp
@@ -220,8 +220,8 @@ SCH_ITEM* SCH_EDITOR_CONTROL::nextMatch(
 
                 for( auto field : cmp->GetFields() )
                 {
-                    if( field->Matches( *aData, nullptr ) )
-                        return field;
+                    if( field.Matches( *aData, nullptr ) )
+                        return &field;
                 }
 
                 for( auto pin : cmp->GetSchPins() )
diff --git a/gerbview/gerbview_frame.cpp b/gerbview/gerbview_frame.cpp
index bb5ec9f8ac..c10014ccb8 100644
--- a/gerbview/gerbview_frame.cpp
+++ b/gerbview/gerbview_frame.cpp
@@ -874,8 +874,8 @@ void GERBVIEW_FRAME::SetPageSettings( const PAGE_INFO& aPageSettings )
     auto drawPanel = static_cast<GERBVIEW_DRAW_PANEL_GAL*>( GetCanvas() );
 
     // Prepare worksheet template
-    auto worksheet =
-            new KIGFX::WS_PROXY_VIEW_ITEM( IU_PER_MILS, &GetPageSettings(), &GetTitleBlock() );
+    auto worksheet = new KIGFX::WS_PROXY_VIEW_ITEM( IU_PER_MILS, &GetPageSettings(),
+                                                    &Prj(), &GetTitleBlock() );
 
     if( screen != NULL )
     {
diff --git a/include/common.h b/include/common.h
index 592524630c..5492e60765 100644
--- a/include/common.h
+++ b/include/common.h
@@ -49,6 +49,7 @@
 #include <typeinfo>
 #include <boost/uuid/uuid.hpp>
 
+class PROJECT;
 class SEARCH_STACK;
 class REPORTER;
 
@@ -310,6 +311,14 @@ const wxString PrePendPath( const wxString& aEnvVar, const wxString& aPriorityPa
  */
 const wxString ExpandEnvVarSubstitutions( const wxString& aString );
 
+/**
+ * Expand '${var-name}' templates in text.  The LocalResolver is given first crack at it,
+ * after which the PROJECT's resolver is called.
+ */
+wxString ExpandTextVars( const wxString& aSource,
+                         const std::function<bool( wxString* )>& aLocalResolver,
+                         const PROJECT* aProject );
+
 /**
  * Replace any environment variables in file-path uris (leaving network-path URIs alone).
  */
diff --git a/include/config_params.h b/include/config_params.h
index 5dc94528e3..46886c1d03 100644
--- a/include/config_params.h
+++ b/include/config_params.h
@@ -50,6 +50,7 @@ using KIGFX::COLOR4D;
 #define GROUP_CVP_EQU       wxT( "/cvpcb/equfiles" )
 
 #define GROUP_SHEET_NAMES   wxT( "/sheetnames" )
+#define GROUP_TEXT_VARS     wxT( "/text_variables" )
 
 #define CONFIG_VERSION      1
 
diff --git a/include/panel_text_variables.h b/include/panel_text_variables.h
new file mode 100644
index 0000000000..12097b3165
--- /dev/null
+++ b/include/panel_text_variables.h
@@ -0,0 +1,65 @@
+/*
+ * This program source code file is part of KICAD, a free EDA CAD application.
+ *
+ * Copyright (C) 2020 Kicad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program 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 2
+ * 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, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#ifndef _PANEL_TEXT_VARIABLES_H_
+#define _PANEL_TEXT_VARIABLES_H_
+
+#include <../common/dialogs/panel_text_variables_base.h>
+#include <wx/valtext.h>
+
+class PROJECT;
+
+
+class PANEL_TEXT_VARIABLES: public PANEL_TEXT_VARIABLES_BASE
+{
+public:
+    PANEL_TEXT_VARIABLES(  wxWindow* aParent, PROJECT* aProject  );
+    ~PANEL_TEXT_VARIABLES() override;
+
+    bool TransferDataToWindow() override;
+    bool TransferDataFromWindow() override;
+
+protected:
+    // Various button callbacks
+    void OnGridCellChange( wxGridEvent& event ) override;
+    void OnGridSize( wxSizeEvent& event ) override;
+    void OnUpdateUI( wxUpdateUIEvent& event ) override;
+    void OnGridCellChanging( wxGridEvent& event );
+    void OnAddTextVar( wxCommandEvent& event ) override;
+    void OnRemoveTextVar( wxCommandEvent& event ) override;
+
+    void AppendTextVar( const wxString& aName, const wxString& aValue );
+
+private:
+    PROJECT*            m_project;
+
+    wxString            m_errorMsg;
+    int                 m_errorRow;
+    int                 m_errorCol;
+
+    wxTextValidator     m_nameValidator;
+
+    bool                m_gridWidthsDirty;
+};
+
+#endif    // _PANEL_TEXT_VARIABLES_H_
diff --git a/include/plotter.h b/include/plotter.h
index b807597598..07cd259b5f 100644
--- a/include/plotter.h
+++ b/include/plotter.h
@@ -1429,9 +1429,9 @@ protected:
 class DXF_PLOTTER : public PLOTTER
 {
 public:
-    DXF_PLOTTER() : textAsLines( false )
+    DXF_PLOTTER() : m_textAsLines( false )
     {
-        textAsLines = true;
+        m_textAsLines = true;
         m_currentColor = COLOR4D::BLACK;
         m_currentLineType = PLOT_DASH_TYPE::SOLID;
         SetUnits( DXF_UNITS::INCHES );
@@ -1453,7 +1453,7 @@ public:
     virtual void SetTextMode( PLOT_TEXT_MODE mode ) override
     {
         if( mode != PLOT_TEXT_MODE::DEFAULT )
-            textAsLines = ( mode != PLOT_TEXT_MODE::NATIVE );
+            m_textAsLines = ( mode != PLOT_TEXT_MODE::NATIVE );
     }
 
     virtual bool StartPlot() override;
@@ -1566,22 +1566,21 @@ public:
     }
 
 protected:
-    bool textAsLines;
-    COLOR4D m_currentColor;
+    bool           m_textAsLines;
+    COLOR4D        m_currentColor;
     PLOT_DASH_TYPE m_currentLineType;
 
-    DXF_UNITS    m_plotUnits;
-    double       m_unitScalingFactor;
-    unsigned int m_measurementDirective;
+    DXF_UNITS      m_plotUnits;
+    double         m_unitScalingFactor;
+    unsigned int   m_measurementDirective;
 };
 
 class TITLE_BLOCK;
-void PlotWorkSheet( PLOTTER* plotter, const TITLE_BLOCK& aTitleBlock,
-                    const PAGE_INFO& aPageInfo,
-                    int aSheetNumber, int aNumberOfSheets,
-                    const wxString &aSheetDesc,
-                    const wxString &aFilename,
-                    const COLOR4D aColor = COLOR4D::UNSPECIFIED );
+
+void PlotWorkSheet( PLOTTER* plotter, const PROJECT* aProject, const TITLE_BLOCK& aTitleBlock,
+                    const PAGE_INFO& aPageInfo, int aSheetNumber, int aNumberOfSheets,
+                    const wxString &aSheetDesc, const wxString &aFilename,
+                    COLOR4D aColor = COLOR4D::UNSPECIFIED );
 
 /** Returns the default plot extension for a format
   */
diff --git a/include/project.h b/include/project.h
index 2334e4875e..247e677cd9 100644
--- a/include/project.h
+++ b/include/project.h
@@ -79,6 +79,9 @@ public:
 
     //-----<Cross Module API>----------------------------------------------------
 
+    VTBL_ENTRY bool TextVarResolver( wxString* aToken ) const;
+    VTBL_ENTRY std::map<wxString, wxString>& GetTextVars() { return m_textVars; }
+
     // VTBL_ENTRY bool MaybeLoadProjectSettings( const std::vector<wxString>& aFileSet );
 
     /**
@@ -337,7 +340,8 @@ private:
     wxFileName      m_project_name;         ///< \<fullpath\>/\<basename\>.pro
     wxString        m_pro_date_and_time;
 
-    std::map<KIID, wxString> m_sheetNames;
+    std::map<KIID, wxString>     m_sheetNames;
+    std::map<wxString, wxString> m_textVars;
 
     /// @see this::SetRString(), GetRString(), and enum RSTRING_T.
     wxString        m_rstrings[RSTRING_COUNT];
@@ -347,38 +351,4 @@ private:
 };
 
 
-//-----<possible futures>---------------------------------------------------------
-
-#if 0
-    /**
-     * Function Value
-     * fetches a project variable @a aVariable and returns true if that variable was
-     * found, else false. If not found, aFetchedValue is not touched.  Any environment
-     * variable is also a project variable.
-     *
-     * @param aVariable is the property or option to look for.
-     * @param aFetchedValue is where to put the value of the property if it exists
-     *  and aFetchedValue is not NULL.
-     * @return bool - true if variable was found, else false.
-     */
-    VTBL_ENTRY bool Value( const wxString& aVariable, wxString* aFetchedValue = NULL );
-
-    /**
-     * Function Substitute
-     * replaces any project variable references found within @a aString with their
-     * values.  Any referenced variable is first sought in the PROJECT space, and if
-     * not found, then sought in the environment.
-     */
-    VTBL_ENTRY const wxString Substitute( const wxString& aString );
-
-    /**
-     * Function SubstituteAndEvaluate
-     * replaces any project variable references found within @a aString with their
-     * values, and evaluates aString as an expression.
-     * Any referenced variable is first sought in the PROJECT space, and if
-     * not found, then sought in the environment.
-     */
-    VTBL_ENTRY const wxString SubstituteAndEvaluate( const wxString& aString );
-#endif
-
 #endif  // PROJECT_H_
diff --git a/include/ws_draw_item.h b/include/ws_draw_item.h
index b199544f72..d1f876171d 100644
--- a/include/ws_draw_item.h
+++ b/include/ws_draw_item.h
@@ -353,20 +353,20 @@ class WS_DRAW_ITEM_LIST
 {
 protected:
     std::vector <WS_DRAW_ITEM_BASE*> m_graphicList;     // Items to draw/plot
-    unsigned m_idx;             // for GetFirst, GetNext functions
-    double   m_milsToIu;        // the scalar to convert pages units ( mils)
-                                // to draw/plot units.
-    int      m_penSize;         // The default line width for drawings.
-                                // used when an item has a pen size = 0
-    int      m_sheetNumber;     // the value of the sheet number, for basic inscriptions
-    int      m_sheetCount;      // the value of the number of sheets, in schematic
-                                // for basic inscriptions, in schematic
-    const TITLE_BLOCK* m_titleBlock;    // for basic inscriptions
-    const wxString* m_paperFormat;      // for basic inscriptions
-    wxString        m_fileName;         // for basic inscriptions
-    wxString        m_sheetFullName;    // for basic inscriptions
-    const wxString* m_sheetLayer;       // for basic inscriptions
-
+    unsigned           m_idx;             // for GetFirst, GetNext functions
+    double             m_milsToIu;        // the scalar to convert pages units ( mils)
+                                          // to draw/plot units.
+    int                m_penSize;         // The default line width for drawings.
+                                          // used when an item has a pen size = 0
+    int                m_sheetNumber;     // the value of the sheet number, for basic inscriptions
+    int                m_sheetCount;      // the value of the number of sheets, in schematic
+                                          // for basic inscriptions, in schematic
+    const TITLE_BLOCK* m_titleBlock;      // for basic inscriptions
+    const wxString*    m_paperFormat;     // for basic inscriptions
+    wxString           m_fileName;        // for basic inscriptions
+    wxString           m_sheetFullName;   // for basic inscriptions
+    const wxString*    m_sheetLayer;      // for basic inscriptions
+    const PROJECT*     m_project;         // for project-based variable substitutions
 
 public:
     WS_DRAW_ITEM_LIST()
@@ -379,6 +379,7 @@ public:
         m_sheetLayer = nullptr;
         m_titleBlock = nullptr;
         m_paperFormat = nullptr;
+        m_project = nullptr;
     }
 
     ~WS_DRAW_ITEM_LIST()
@@ -388,6 +389,8 @@ public:
         //     delete item;
     }
 
+    void SetProject( const PROJECT* aProject ) { m_project = aProject; }
+
     /**
      * Set the title block (mainly for page layout editor)
      */
diff --git a/include/ws_painter.h b/include/ws_painter.h
index 6e21d97626..2656abf17a 100644
--- a/include/ws_painter.h
+++ b/include/ws_painter.h
@@ -155,6 +155,6 @@ private:
 void PrintPageLayout( wxDC* aDC, const PAGE_INFO& aPageInfo, const wxString& aFullSheetName,
                       const wxString& aFileName, const TITLE_BLOCK& aTitleBlock, int aSheetCount,
                       int aSheetNumber, int aPenWidth, double aScalar, COLOR4D aColor,
-                      const wxString& aSheetLayer = wxEmptyString );
+                      const PROJECT* aProject, const wxString& aSheetLayer = wxEmptyString );
 
 #endif // WS_PAINTER_H
diff --git a/include/ws_proxy_view_item.h b/include/ws_proxy_view_item.h
index 6f58eaa986..84be559494 100644
--- a/include/ws_proxy_view_item.h
+++ b/include/ws_proxy_view_item.h
@@ -32,7 +32,6 @@ class PAGE_INFO;
 class TITLE_BLOCK;
 class WS_DRAW_ITEM_LINE;
 class WS_DRAW_ITEM_RECT;
-class WS_DRAW_ITEM_POLYGON;
 class WS_DRAW_ITEM_TEXT;
 class WS_DRAW_ITEM_BITMAP;
 
@@ -44,67 +43,32 @@ class GAL;
 class WS_PROXY_VIEW_ITEM : public EDA_ITEM
 {
 public:
-    WS_PROXY_VIEW_ITEM( int aScaleFactor, const PAGE_INFO* aPageInfo,  const TITLE_BLOCK* aTitleBlock );
+    WS_PROXY_VIEW_ITEM( int aScaleFactor, const PAGE_INFO* aPageInfo, const PROJECT* aProject,
+                        const TITLE_BLOCK* aTitleBlock );
 
     /**
      * Function SetFileName()
      * Sets the file name displayed in the title block.
-     *
-     * @param aFileName is the new file name.
      */
-    void SetFileName( const std::string& aFileName )
-    {
-        m_fileName = aFileName;
-    }
+    void SetFileName( const std::string& aFileName ) { m_fileName = aFileName; }
 
     /**
      * Function SetSheetName()
      * Sets the sheet name displayed in the title block.
-     *
-     * @param aSheetName is the new sheet name.
      */
-    void SetSheetName( const std::string& aSheetName )
-    {
-        m_sheetName = aSheetName;
-    }
-
-    /**
-     * Function SetPageInfo()
-     * Changes the PAGE_INFO object used to draw the worksheet.
-     *
-     * @param aPageInfo is the new PAGE_INFO object.
-     */
-    void SetPageInfo( const PAGE_INFO* aPageInfo );
-
-    /**
-     * Function SetTitleBlock()
-     * Changes the TITLE_BLOCK object used to draw the worksheet.
-     *
-     * @param aTitleBlock is the new TITLE_BLOCK object.
-     */
-    void SetTitleBlock( const TITLE_BLOCK* aTitleBlock );
+    void SetSheetName( const std::string& aSheetName ) { m_sheetName = aSheetName; }
 
     /**
      * Function SetSheetNumber()
      * Changes the sheet number displayed in the title block.
-     *
-     * @param aSheetNumber is the new sheet number.
      */
-    void SetSheetNumber( int aSheetNumber )
-    {
-        m_sheetNumber = aSheetNumber;
-    }
+    void SetSheetNumber( int aSheetNumber ) { m_sheetNumber = aSheetNumber; }
 
     /**
      * Function SetSheetCount()
      * Changes the sheets count number displayed in the title block.
-     *
-     * @param aSheetCount is the new sheets count number.
      */
-    void SetSheetCount( int aSheetCount )
-    {
-        m_sheetCount = aSheetCount;
-    }
+    void SetSheetCount( int aSheetCount ) { m_sheetCount = aSheetCount; }
 
     /**
      * Can be used to override which layer ID is used for worksheet item colors
@@ -140,28 +104,18 @@ public:
 protected:
     /// the factor between mils (units used in worksheet and internal units)
     /// it is the value IU_PER_MILS used in the caller
-    int m_mils2IUscalefactor;
+    int                m_mils2IUscalefactor;
 
-    /// File name displayed in the title block
-    std::string m_fileName;
-
-    /// Sheet name displayed in the title block
-    std::string m_sheetName;
-
-    /// Title block that contains properties of the title block displayed in the worksheet.
+    std::string        m_fileName;
+    std::string        m_sheetName;
     const TITLE_BLOCK* m_titleBlock;
-
-    /// Worksheet page information.
-    const PAGE_INFO* m_pageInfo;
-
-    /// Sheet number displayed in the title block.
-    int m_sheetNumber;
-
-    /// Sheets count number displayed in the title block.
-    int m_sheetCount;
+    const PAGE_INFO*   m_pageInfo;
+    int                m_sheetNumber;
+    int                m_sheetCount;
+    const PROJECT*     m_project;
 
     /// Layer that is used for worksheet color (LAYER_WORKSHEET is always used for visibility)
-    int m_colorLayer;
+    int                m_colorLayer;
 };
 }
 
diff --git a/pagelayout_editor/pl_draw_panel_gal.cpp b/pagelayout_editor/pl_draw_panel_gal.cpp
index 0ab50bea59..f5ef791921 100644
--- a/pagelayout_editor/pl_draw_panel_gal.cpp
+++ b/pagelayout_editor/pl_draw_panel_gal.cpp
@@ -26,6 +26,7 @@
 #include <ws_data_model.h>
 #include <ws_painter.h>
 #include <pgm_base.h>
+#include <kiway.h>
 #include <pl_editor_frame.h>
 #include <pl_editor_settings.h>
 #include <settings/settings_manager.h>
@@ -98,6 +99,7 @@ void PL_DRAW_PANEL_GAL::DisplayWorksheet()
     WS_DRAW_ITEM_LIST dummy;
     dummy.SetPaperFormat( &m_edaFrame->GetPageSettings().GetType() );
     dummy.SetTitleBlock( &m_edaFrame->GetTitleBlock() );
+    dummy.SetProject( &m_edaFrame->Prj() );
 
     for( WS_DATA_ITEM* dataItem : model.GetItems() )
         dataItem->SyncDrawItems( &dummy, m_view );
diff --git a/pcbnew/class_board.cpp b/pcbnew/class_board.cpp
index bbe20f896a..a37a484b7f 100644
--- a/pcbnew/class_board.cpp
+++ b/pcbnew/class_board.cpp
@@ -100,8 +100,10 @@ wxPoint BOARD_ITEM::ZeroOffset( 0, 0 );
 static PCBNEW_SETTINGS dummyGeneralSettings;
 
 BOARD::BOARD() :
-    BOARD_ITEM_CONTAINER( (BOARD_ITEM*) NULL, PCB_T ),
-        m_paper( PAGE_INFO::A4 ), m_NetInfo( this )
+        BOARD_ITEM_CONTAINER( (BOARD_ITEM*) NULL, PCB_T ),
+        m_paper( PAGE_INFO::A4 ),
+        m_NetInfo( this ),
+        m_project( nullptr )
 {
     // we have not loaded a board yet, assume latest until then.
     m_fileFormatVersionAtLoad = LEGACY_BOARD_FILE_VERSION;
diff --git a/pcbnew/class_board.h b/pcbnew/class_board.h
index f69b7731b2..caa1835c0d 100644
--- a/pcbnew/class_board.h
+++ b/pcbnew/class_board.h
@@ -57,6 +57,7 @@ class REPORTER;
 class SHAPE_POLY_SET;
 class CONNECTIVITY_DATA;
 class COMPONENT;
+class PROJECT;
 
 // Forward declare endpoint from class_track.h
 enum ENDPOINT_T : int;
@@ -185,20 +186,21 @@ private:
 
     LAYER                   m_Layer[PCB_LAYER_ID_COUNT];
 
-                                                    // if true m_highLight_NetCode is used
+                                                        // if true m_highLight_NetCode is used
     HIGH_LIGHT_INFO         m_highLight;                // current high light data
     HIGH_LIGHT_INFO         m_highLightPrevious;        // a previously stored high light data
 
-    int                     m_fileFormatVersionAtLoad;  ///< the version loaded from the file
+    int                     m_fileFormatVersionAtLoad;  // the version loaded from the file
 
     std::shared_ptr<CONNECTIVITY_DATA>      m_connectivity;
 
     BOARD_DESIGN_SETTINGS   m_designSettings;
-    PCBNEW_SETTINGS*        m_generalSettings;      ///< reference only; I have no ownership
+    PCBNEW_SETTINGS*        m_generalSettings;      // reference only; I have no ownership
     PAGE_INFO               m_paper;
-    TITLE_BLOCK             m_titles;               ///< text in lower right of screen and plots
+    TITLE_BLOCK             m_titles;               // text in lower right of screen and plots
     PCB_PLOT_PARAMS         m_plotOptions;
-    NETINFO_LIST            m_NetInfo;              ///< net info list (name, design constraints ..
+    NETINFO_LIST            m_NetInfo;              // net info list (name, design constraints ..
+    PROJECT*                m_project;              // project this board is a part of (if any)
 
 
     // The default copy constructor & operator= are inadequate,
@@ -303,10 +305,7 @@ public:
      * returns list of missing connections between components/tracks.
      * @return an object that contains informations about missing connections.
      */
-    std::shared_ptr<CONNECTIVITY_DATA> GetConnectivity() const
-    {
-        return m_connectivity;
-    }
+    std::shared_ptr<CONNECTIVITY_DATA> GetConnectivity() const { return m_connectivity; }
 
     /**
      * Builds or rebuilds the board connectivity database for the board,
@@ -327,6 +326,9 @@ public:
      */
     void DeleteZONEOutlines();
 
+    PROJECT* GetProject() const            { return m_project; }
+    void SetProject( PROJECT* aProject )   { m_project = aProject; }
+
     /**
      * Function SetAuxOrigin
      * sets the origin point used for plotting.
@@ -355,16 +357,8 @@ public:
      * Function GetHighLightNetCode
      * @return netcode of net to highlight (-1 when no net selected)
      */
-    int GetHighLightNetCode() const { return m_highLight.m_netCode; }
-
-    /**
-     * Function SetHighLightNet
-     * @param aNetCode = netcode of net to highlight
-     */
-    void SetHighLightNet( int aNetCode)
-    {
-        m_highLight.m_netCode = aNetCode;
-    }
+    int GetHighLightNetCode() const       { return m_highLight.m_netCode; }
+    void SetHighLightNet( int aNetCode)   { m_highLight.m_netCode = aNetCode; }
 
     /**
      * Function IsHighLightNetON
@@ -372,25 +366,19 @@ public:
      */
     bool IsHighLightNetON() const { return m_highLight.m_highLightOn; }
 
-    /**
-     * Function HighLightOFF
-     * Disable highlight.
-     */
-    void HighLightOFF() { m_highLight.m_highLightOn = false; }
-
     /**
      * Function HighLightON
      * Enable highlight.
      * if m_highLight_NetCode >= 0, this net will be highlighted
      */
     void HighLightON() { m_highLight.m_highLightOn = true; }
+    void HighLightOFF() { m_highLight.m_highLightOn = false; }
 
     /**
      * Function GetCopperLayerCount
      * @return int - The number of copper layers in the BOARD.
      */
     int  GetCopperLayerCount() const;
-
     void SetCopperLayerCount( int aCount );
 
     /**
@@ -575,25 +563,13 @@ public:
 
     /**
      * Function GetLayerID
-     * returns the ID of a layer given by aLayerName.  Copper layers may
-     * have custom names.
-     *
-     * @param aLayerName = A layer name, like wxT("B.Cu"), etc.
-     *
-     * @return PCB_LAYER_ID -   the layer id, which for copper layers may
-     *                      be custom, else standard.
+     * returns the ID of a layer.  Copper layers may have custom names.
      */
     const PCB_LAYER_ID GetLayerID( const wxString& aLayerName ) const;
 
     /**
      * Function GetLayerName
-     * returns the name of a layer given by aLayer.  Copper layers may
-     * have custom names.
-     *
-     * @param aLayer = A layer, like B_Cu, etc.
-     *
-     * @return wxString -   the layer name, which for copper layers may
-     *                      be custom, else standard.
+     * returns the name of a layer.  Copper layers may have custom names.
      */
     const wxString GetLayerName( PCB_LAYER_ID aLayer ) const;
 
diff --git a/pcbnew/class_module.cpp b/pcbnew/class_module.cpp
index 827b28fe6a..6c3be99527 100644
--- a/pcbnew/class_module.cpp
+++ b/pcbnew/class_module.cpp
@@ -1330,12 +1330,12 @@ BOARD_ITEM* MODULE::DuplicateItem( const BOARD_ITEM* aItem, bool aIncrementPadNu
 
         if( new_text->GetType() == TEXTE_MODULE::TEXT_is_REFERENCE )
         {
-            new_text->SetText( wxT( "%R" ) );
+            new_text->SetText( wxT( "${REFERENCE}" ) );
             new_text->SetType( TEXTE_MODULE::TEXT_is_DIVERS );
         }
         else if( new_text->GetType() == TEXTE_MODULE::TEXT_is_VALUE )
         {
-            new_text->SetText( wxT( "%V" ) );
+            new_text->SetText( wxT( "${VALUE}" ) );
             new_text->SetType( TEXTE_MODULE::TEXT_is_DIVERS );
         }
 
diff --git a/pcbnew/class_pcb_text.cpp b/pcbnew/class_pcb_text.cpp
index 92961210d9..00e87354ec 100644
--- a/pcbnew/class_pcb_text.cpp
+++ b/pcbnew/class_pcb_text.cpp
@@ -61,6 +61,26 @@ TEXTE_PCB::~TEXTE_PCB()
 }
 
 
+wxString TEXTE_PCB::GetShownText() const
+{
+    const BOARD* board = static_cast<BOARD*>( GetParent() );
+    wxASSERT( board );
+
+    auto moduleResolver = [ this ]( wxString* token ) -> bool
+                          {
+                              if( token->IsSameAs( wxT( "LAYER" ) ) )
+                              {
+                                  *token = GetLayerName();
+                                  return true;
+                              }
+
+                              return false;
+                          };
+
+    return ExpandTextVars( GetText(), moduleResolver, board->GetProject() );
+}
+
+
 void TEXTE_PCB::SetTextAngle( double aAngle )
 {
     EDA_TEXT::SetTextAngle( NormalizeAngle360Min( aAngle ) );
@@ -178,9 +198,12 @@ EDA_ITEM* TEXTE_PCB::Clone() const
     return new TEXTE_PCB( *this );
 }
 
+
 void TEXTE_PCB::SwapData( BOARD_ITEM* aImage )
 {
     assert( aImage->Type() == PCB_TEXT_T );
 
     std::swap( *((TEXTE_PCB*) this), *((TEXTE_PCB*) aImage) );
 }
+
+
diff --git a/pcbnew/class_pcb_text.h b/pcbnew/class_pcb_text.h
index 9ddcff1f6c..d626ea59db 100644
--- a/pcbnew/class_pcb_text.h
+++ b/pcbnew/class_pcb_text.h
@@ -53,6 +53,8 @@ public:
         return aItem && PCB_TEXT_T == aItem->Type();
     }
 
+    wxString GetShownText() const override;
+
     bool Matches( wxFindReplaceData& aSearchData, void* aAuxData ) override
     {
         return BOARD_ITEM::Matches( GetShownText(), aSearchData );
diff --git a/pcbnew/class_text_mod.cpp b/pcbnew/class_text_mod.cpp
index 2e9d70f2f7..92309b594e 100644
--- a/pcbnew/class_text_mod.cpp
+++ b/pcbnew/class_text_mod.cpp
@@ -28,6 +28,7 @@
 #include <trigo.h>
 #include <gr_text.h>
 #include <kicad_string.h>
+#include <common.h>
 #include <richio.h>
 #include <macros.h>
 #include <pcb_edit_frame.h>
@@ -41,7 +42,7 @@
 #include <pgm_base.h>
 #include <settings/color_settings.h>
 #include <settings/settings_manager.h>
-
+#include <kiway.h>
 
 TEXTE_MODULE::TEXTE_MODULE( MODULE* parent, TEXT_TYPE text_type ) :
     BOARD_ITEM( parent, PCB_MODULE_TEXT_T ),
@@ -274,10 +275,10 @@ void TEXTE_MODULE::Print( PCB_BASE_FRAME* aFrame, wxDC* aDC, const wxPoint& aOff
       || ( IsBackLayer( text_layer ) && !brd->IsElementVisible( LAYER_MOD_TEXT_BK ) ) )
         return;
 
-    if( !brd->IsElementVisible( LAYER_MOD_REFERENCES ) && GetText() == wxT( "%R" ) )
+    if( !brd->IsElementVisible( LAYER_MOD_REFERENCES ) && GetText() == wxT( "${REFERENCE}" ) )
         return;
 
-    if( !brd->IsElementVisible( LAYER_MOD_VALUES ) && GetText() == wxT( "%V" ) )
+    if( !brd->IsElementVisible( LAYER_MOD_VALUES ) && GetText() == wxT( "${VALUE}" ) )
         return;
 
     // Invisible texts are still drawn (not plotted) in LAYER_MOD_TEXT_INVISIBLE
@@ -462,11 +463,11 @@ unsigned int TEXTE_MODULE::ViewGetLOD( int aLayer, KIGFX::VIEW* aView ) const
         return HIDE;
 
     // Handle Render tab switches
-    if( ( m_Type == TEXT_is_VALUE || GetText() == wxT( "%V" ) )
+    if( ( m_Type == TEXT_is_VALUE || GetText() == wxT( "${VALUE}" ) )
             && !aView->IsLayerVisible( LAYER_MOD_VALUES ) )
         return HIDE;
 
-    if( ( m_Type == TEXT_is_REFERENCE || GetText() == wxT( "%R" ) )
+    if( ( m_Type == TEXT_is_REFERENCE || GetText() == wxT( "${REFERENCE}" ) )
             && !aView->IsLayerVisible( LAYER_MOD_REFERENCES ) )
         return HIDE;
 
@@ -489,57 +490,35 @@ unsigned int TEXTE_MODULE::ViewGetLOD( int aLayer, KIGFX::VIEW* aView ) const
 
 wxString TEXTE_MODULE::GetShownText() const
 {
-    /* First order optimization: no % means that no processing is
-     * needed; just hope that RVO and copy constructor implementation
-     * avoid to copy the whole block; anyway it should be better than
-     * rebuild the string one character at a time...
-     * Also it seems wise to only expand macros in user text (but there
-     * is no technical reason, probably) */
+    const MODULE* module = static_cast<MODULE*>( GetParent() );
+     wxASSERT( module );
 
-    if( (m_Type != TEXT_is_DIVERS) || (wxString::npos == GetText().find('%')) )
-        return GetText();
+    const BOARD* board = static_cast<BOARD*>( module->GetParent() );
+    wxASSERT( board );
 
-    wxString newbuf;
-    const MODULE *module = static_cast<MODULE*>( GetParent() );
+    auto moduleResolver = [ this, module ]( wxString* token ) -> bool
+                          {
+                              if( module )
+                              {
+                                  if( token->IsSameAs( wxT( "REFERENCE" ) ) )
+                                  {
+                                      *token = module->GetReference();
+                                      return true;
+                                  }
+                                  else if( token->IsSameAs( wxT( "VALUE" ) ) )
+                                  {
+                                      *token = module->GetValue();
+                                      return true;
+                                  }
+                                  else if( token->IsSameAs( wxT( "LAYER" ) ) )
+                                  {
+                                      *token = GetLayerName();
+                                      return true;
+                                  }
+                              }
 
-    for( wxString::const_iterator it = GetText().begin(); it != GetText().end(); ++it )
-    {
-        // Process '%' and copy everything else
-        if( *it != '%' )
-            newbuf.append(*it);
-        else
-        {
-            /* Look at the next character (if is it there) and append
-             * its expansion */
-            ++it;
+                              return false;
+                          };
 
-            if( it != GetText().end() )
-            {
-                switch( char(*it) )
-                {
-                case '%':
-                    newbuf.append( '%' );
-                    break;
-
-                case 'R':
-                    if( module )
-                        newbuf.append( module->GetReference() );
-                    break;
-
-                case 'V':
-                    if( module )
-                        newbuf.append( module->GetValue() );
-                    break;
-
-                default:
-                    newbuf.append( '?' );
-                    break;
-                }
-            }
-            else
-                break; // The string is over and we can't ++ anymore
-        }
-    }
-
-    return newbuf;
+    return ExpandTextVars( GetText(), moduleResolver, board->GetProject() );
 }
diff --git a/pcbnew/dialogs/dialog_board_setup.cpp b/pcbnew/dialogs/dialog_board_setup.cpp
index 695e60997c..e5f33fefe7 100644
--- a/pcbnew/dialogs/dialog_board_setup.cpp
+++ b/pcbnew/dialogs/dialog_board_setup.cpp
@@ -29,13 +29,11 @@
 #include <drc/drc_item.h>
 #include <dialog_import_settings.h>
 #include <panel_setup_severities.h>
+#include <panel_text_variables.h>
 
 #include "dialog_board_setup.h"
 
 
-bool g_macHack;
-
-
 DIALOG_BOARD_SETUP::DIALOG_BOARD_SETUP( PCB_EDIT_FRAME* aFrame ) :
         PAGED_DIALOG( aFrame, _( "Board Setup" ), _( "Import Settings from Another Project..." ) ),
         m_frame( aFrame )
@@ -53,6 +51,8 @@ DIALOG_BOARD_SETUP::DIALOG_BOARD_SETUP( PCB_EDIT_FRAME* aFrame ) :
     m_severities = new PANEL_SETUP_SEVERITIES( this, dummyItem, bds.m_DRCSeverities,
                                                DRCE_FIRST, DRCE_LAST );
 
+    m_textVars = new PANEL_TEXT_VARIABLES( this, &Prj() );
+
     /*
      * WARNING: If you change page names you MUST update calls to DoShowBoardSetupDialog().
      */
@@ -73,12 +73,17 @@ DIALOG_BOARD_SETUP::DIALOG_BOARD_SETUP( PCB_EDIT_FRAME* aFrame ) :
     m_treebook->AddSubPage( m_netclasses,  _( "Net Classes" ) );
     m_treebook->AddSubPage( m_severities, _( "Violation Severity" ) );
 
+    m_treebook->AddPage( new wxPanel( this ), _( "Project" ) );
+    m_treebook->AddSubPage( m_textVars, _( "Text Variables" ) );
+
 	// Connect Events
 	m_treebook->Connect( wxEVT_TREEBOOK_PAGE_CHANGED,
                          wxBookCtrlEventHandler( DIALOG_BOARD_SETUP::OnPageChange ), NULL, this );
 
 	FinishDialogSettings();
-	g_macHack = true;
+
+	for( int i = 0; i < m_treebook->GetPageCount(); ++i )
+	    m_macHack.push_back( true );
 }
 
 
@@ -95,13 +100,18 @@ void DIALOG_BOARD_SETUP::OnPageChange( wxBookCtrlEvent& event )
         m_physicalStackup->OnLayersOptionsChanged( m_layers->GetUILayerMask() );
 
 #ifdef __WXMAC__
-    // Work around an OSX bug where the wxGrid children don't get placed correctly
-    if( g_macHack && m_treebook->GetPage( event.GetSelection() ) == m_tracksAndVias )
-    {
-        m_tracksAndVias->SetSize( wxSize( m_tracksAndVias->GetSize().x - 1,
-                                          m_tracksAndVias->GetSize().y + 2 ) );
+    // Work around an OSX bug where the wxGrid children don't get placed correctly until
+    // the first resize event
+    int page = event.GetSelection();
 
-        g_macHack = false;
+    if( m_macHack[ page ] )
+    {
+        wxSize pageSize = m_treebook->GetPage( page )->GetSize();
+        pageSize.x -= 1;
+        pageSize.y += 2;
+
+        m_treebook->GetPage( page )->SetSize( pageSize );
+        m_macHack[ page ] = false;
     }
 #endif
 }
diff --git a/pcbnew/dialogs/dialog_board_setup.h b/pcbnew/dialogs/dialog_board_setup.h
index 446fec8064..fa4a7d1f94 100644
--- a/pcbnew/dialogs/dialog_board_setup.h
+++ b/pcbnew/dialogs/dialog_board_setup.h
@@ -32,6 +32,7 @@ class PANEL_SETUP_TRACKS_AND_VIAS;
 class PANEL_SETUP_MASK_AND_PASTE;
 class PANEL_SETUP_BOARD_STACKUP;
 class PANEL_SETUP_SEVERITIES;
+class PANEL_TEXT_VARIABLES;
 
 
 class DIALOG_BOARD_SETUP : public PAGED_DIALOG
@@ -53,6 +54,9 @@ protected:
     PANEL_SETUP_MASK_AND_PASTE*      m_maskAndPaste;
     PANEL_SETUP_BOARD_STACKUP*       m_physicalStackup;
     PANEL_SETUP_SEVERITIES*          m_severities;
+    PANEL_TEXT_VARIABLES*            m_textVars;
+
+    std::vector<bool>                m_macHack;
 
     // event handlers
     void OnPageChange( wxBookCtrlEvent& event );
diff --git a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.cpp b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.cpp
index f6b657fc7f..d0093a2b26 100644
--- a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.cpp
+++ b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.cpp
@@ -53,7 +53,6 @@
 
 int DIALOG_FOOTPRINT_BOARD_EDITOR::m_page = 0;     // remember the last open page during session
 
-
 DIALOG_FOOTPRINT_BOARD_EDITOR::DIALOG_FOOTPRINT_BOARD_EDITOR( PCB_EDIT_FRAME* aParent,
                                                               MODULE* aModule ) :
     DIALOG_FOOTPRINT_BOARD_EDITOR_BASE( aParent ),
@@ -156,6 +155,9 @@ DIALOG_FOOTPRINT_BOARD_EDITOR::DIALOG_FOOTPRINT_BOARD_EDITOR( PCB_EDIT_FRAME* aP
     m_buttonRemove->SetBitmap( KiBitmap( trash_xpm ) );
 
     FinishDialogSettings();
+
+    for( int i = 0; i < m_NoteBook->GetPageCount(); ++i )
+   	    m_macHack.push_back( true );
 }
 
 
@@ -921,6 +923,29 @@ void DIALOG_FOOTPRINT_BOARD_EDITOR::OnGridSize( wxSizeEvent& aEvent )
 }
 
 
+void DIALOG_FOOTPRINT_BOARD_EDITOR::OnPageChange( wxNotebookEvent& aEvent )
+{
+    int page = aEvent.GetSelection();
+
+    // Shouldn't be necessary, but is on at least OSX
+    if( page >= 0 )
+        m_NoteBook->ChangeSelection( (unsigned) page );
+
+#ifdef __WXMAC__
+    // Work around an OSX bug where the wxGrid children don't get placed correctly until
+    // the first resize event
+    if( m_macHack[ page ] )
+    {
+        wxSize pageSize = m_NoteBook->GetPage( page )->GetSize();
+        pageSize.x -= 1;
+
+        m_NoteBook->GetPage( page )->SetSize( pageSize );
+        m_macHack[ page ] = false;
+    }
+#endif
+}
+
+
 void DIALOG_FOOTPRINT_BOARD_EDITOR::updateOrientationControl()
 {
     KIUI::ValidatorTransferToWindowWithoutEvents( m_OrientValidator );
diff --git a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.h b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.h
index 845ce9ba19..4dcfc7a16e 100644
--- a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.h
+++ b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor.h
@@ -66,6 +66,7 @@ private:
     bool                             m_initialFocus;
 
     bool                             m_inSelect;
+    std::vector<bool>                m_macHack;
 
 public:
     // The dialog can be closed for several reasons.
@@ -106,6 +107,7 @@ private:
     void OnAddField( wxCommandEvent&  ) override;
     void OnDeleteField( wxCommandEvent&  ) override;
     void OnUpdateUI( wxUpdateUIEvent&  ) override;
+    void OnPageChange( wxNotebookEvent& event ) override;
 
     void select3DModel( int aModelIdx );
 
diff --git a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.cpp b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.cpp
index e3546bdcd4..25609eeeb1 100644
--- a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.cpp
+++ b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.cpp
@@ -552,6 +552,7 @@ DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::DIALOG_FOOTPRINT_BOARD_EDITOR_BASE( wxWindow
 	// Connect Events
 	this->Connect( wxEVT_INIT_DIALOG, wxInitDialogEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnInitDlg ) );
 	this->Connect( wxEVT_UPDATE_UI, wxUpdateUIEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnUpdateUI ) );
+	m_NoteBook->Connect( wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED, wxNotebookEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnPageChange ), NULL, this );
 	m_itemsGrid->Connect( wxEVT_SIZE, wxSizeEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnGridSize ), NULL, this );
 	m_bpAdd->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnAddField ), NULL, this );
 	m_bpDelete->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnDeleteField ), NULL, this );
@@ -578,6 +579,7 @@ DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::~DIALOG_FOOTPRINT_BOARD_EDITOR_BASE()
 	// Disconnect Events
 	this->Disconnect( wxEVT_INIT_DIALOG, wxInitDialogEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnInitDlg ) );
 	this->Disconnect( wxEVT_UPDATE_UI, wxUpdateUIEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnUpdateUI ) );
+	m_NoteBook->Disconnect( wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED, wxNotebookEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnPageChange ), NULL, this );
 	m_itemsGrid->Disconnect( wxEVT_SIZE, wxSizeEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnGridSize ), NULL, this );
 	m_bpAdd->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnAddField ), NULL, this );
 	m_bpDelete->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_FOOTPRINT_BOARD_EDITOR_BASE::OnDeleteField ), NULL, this );
diff --git a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.fbp b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.fbp
index 6a41ecaa61..59d8077f61 100644
--- a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.fbp
+++ b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.fbp
@@ -117,6 +117,7 @@
                         <property name="window_extra_style"></property>
                         <property name="window_name"></property>
                         <property name="window_style"></property>
+                        <event name="OnNotebookPageChanged">OnPageChange</event>
                         <object class="notebookpage" expanded="1">
                             <property name="bitmap"></property>
                             <property name="label">General</property>
diff --git a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.h b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.h
index fc7a42fbc3..0b00b554ed 100644
--- a/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.h
+++ b/pcbnew/dialogs/dialog_edit_footprint_for_BoardEditor_base.h
@@ -123,6 +123,7 @@ class DIALOG_FOOTPRINT_BOARD_EDITOR_BASE : public DIALOG_SHIM
 		// Virtual event handlers, overide them in your derived class
 		virtual void OnInitDlg( wxInitDialogEvent& event ) { event.Skip(); }
 		virtual void OnUpdateUI( wxUpdateUIEvent& event ) { event.Skip(); }
+		virtual void OnPageChange( wxNotebookEvent& event ) { event.Skip(); }
 		virtual void OnGridSize( wxSizeEvent& event ) { event.Skip(); }
 		virtual void OnAddField( wxCommandEvent& event ) { event.Skip(); }
 		virtual void OnDeleteField( wxCommandEvent& event ) { event.Skip(); }
diff --git a/pcbnew/dialogs/dialog_global_edit_text_and_graphics.cpp b/pcbnew/dialogs/dialog_global_edit_text_and_graphics.cpp
index fcb1ad6ed3..39c45b82f7 100644
--- a/pcbnew/dialogs/dialog_global_edit_text_and_graphics.cpp
+++ b/pcbnew/dialogs/dialog_global_edit_text_and_graphics.cpp
@@ -373,9 +373,9 @@ bool DIALOG_GLOBAL_EDIT_TEXT_AND_GRAPHICS::TransferDataFromWindow()
                 // use the dynamic_cast to move through the type tree anyway.
                 const wxString text = dynamic_cast<EDA_TEXT*>( boardItem )->GetText();
 
-                if( m_references->GetValue() && text == wxT( "%R" ) )
+                if( m_references->GetValue() && text == wxT( "${REFERENCE}" ) )
                     visitItem( commit, boardItem );
-                else if( m_values->GetValue() && text == wxT( "%V" ) )
+                else if( m_values->GetValue() && text == wxT( "${VALUE}" ) )
                     visitItem( commit, boardItem );
                 else if( m_otherFields->GetValue() )
                     visitItem( commit, boardItem );
diff --git a/pcbnew/legacy_plugin.cpp b/pcbnew/legacy_plugin.cpp
index 857c64caee..dd535022c7 100644
--- a/pcbnew/legacy_plugin.cpp
+++ b/pcbnew/legacy_plugin.cpp
@@ -88,6 +88,7 @@
 #include <build_version.h>
 #include <confirm.h>
 #include <math/util.h>      // for KiROUND
+#include <template_fieldnames.h>
 
 typedef LEGACY_PLUGIN::BIU      BIU;
 
@@ -1781,7 +1782,8 @@ void LEGACY_PLUGIN::loadMODULE_TEXT( TEXTE_MODULE* aText )
     // convert the "quoted, escaped, UTF8, text" to a wxString, find it by skipping
     // as far forward as needed until the first double quote.
     txt_end = data + ReadDelimitedText( &m_field, data );
-
+    m_field.Replace( "%V", "${VALUE}" );
+    m_field.Replace( "%R", "${REFERENCE}" );
     aText->SetText( m_field );
 
     // after switching to strtok, there's no easy coming back because of the
diff --git a/pcbnew/pcb_edit_frame.cpp b/pcbnew/pcb_edit_frame.cpp
index 207bec5840..92be2ee736 100644
--- a/pcbnew/pcb_edit_frame.cpp
+++ b/pcbnew/pcb_edit_frame.cpp
@@ -341,6 +341,7 @@ void PCB_EDIT_FRAME::SetBoard( BOARD* aBoard )
 {
     PCB_BASE_EDIT_FRAME::SetBoard( aBoard );
 
+    aBoard->SetProject( &Prj() );
     aBoard->GetConnectivity()->Build( aBoard );
 
     // reload the worksheet
@@ -369,7 +370,7 @@ void PCB_EDIT_FRAME::SetPageSettings( const PAGE_INFO& aPageSettings )
     // Prepare worksheet template
     KIGFX::WS_PROXY_VIEW_ITEM* worksheet;
     worksheet = new KIGFX::WS_PROXY_VIEW_ITEM( IU_PER_MILS ,&m_Pcb->GetPageSettings(),
-                                               &m_Pcb->GetTitleBlock() );
+                                               m_Pcb->GetProject(), &m_Pcb->GetTitleBlock() );
     worksheet->SetSheetName( std::string( GetScreenDesc().mb_str() ) );
 
     BASE_SCREEN* screen = GetScreen();
diff --git a/pcbnew/pcb_parser.cpp b/pcbnew/pcb_parser.cpp
index 2619974852..ff05894593 100644
--- a/pcbnew/pcb_parser.cpp
+++ b/pcbnew/pcb_parser.cpp
@@ -50,6 +50,7 @@
 #include <zones.h>
 #include <pcb_parser.h>
 #include <convert_basic_shapes_to_polygon.h>    // for RECT_CHAMFER_POSITIONS definition
+#include <template_fieldnames.h>
 
 using namespace PCB_KEYS_T;
 
@@ -2624,7 +2625,10 @@ TEXTE_MODULE* PCB_PARSER::parseTEXTE_MODULE()
 
     NeedSYMBOLorNUMBER();
 
-    text->SetText( FromUTF8() );
+    wxString value = FromUTF8();
+    value.Replace( "%V", "${VALUE}" );
+    value.Replace( "%R", "${REFERENCE}" );
+    text->SetText( value );
     NeedLEFT();
     token = NextTok();
 
diff --git a/pcbnew/plot_board_layers.cpp b/pcbnew/plot_board_layers.cpp
index 4383f60366..ebb5316532 100644
--- a/pcbnew/plot_board_layers.cpp
+++ b/pcbnew/plot_board_layers.cpp
@@ -1232,8 +1232,8 @@ PLOTTER* StartPlotBoard( BOARD *aBoard, PCB_PLOT_PARAMS *aPlotOpts, int aLayer,
         // Plot the frame reference if requested
         if( aPlotOpts->GetPlotFrameRef() )
         {
-            PlotWorkSheet( plotter, aBoard->GetTitleBlock(), aBoard->GetPageSettings(),
-                           1, 1, aSheetDesc, aBoard->GetFileName() );
+            PlotWorkSheet( plotter, aBoard->GetProject(), aBoard->GetTitleBlock(),
+                           aBoard->GetPageSettings(), 1, 1, aSheetDesc, aBoard->GetFileName() );
 
             if( aPlotOpts->GetMirror() )
                 initializePlotter( plotter, aBoard, aPlotOpts );
diff --git a/pcbnew/plot_brditems_plotter.cpp b/pcbnew/plot_brditems_plotter.cpp
index 30b9e4b8f4..22e3bd28be 100644
--- a/pcbnew/plot_brditems_plotter.cpp
+++ b/pcbnew/plot_brditems_plotter.cpp
@@ -314,10 +314,10 @@ bool BRDITEMS_PLOTTER::PlotAllTextsModule( MODULE* aModule )
         if( !m_layerMask[textLayer] )
             continue;
 
-        if( textModule->GetText() == wxT( "%R" ) && !GetPlotReference() )
+        if( textModule->GetText() == wxT( "${REFERENCE}" ) && !GetPlotReference() )
             continue;
 
-        if( textModule->GetText() == wxT( "%V" ) && !GetPlotValue() )
+        if( textModule->GetText() == wxT( "${VALUE}" ) && !GetPlotValue() )
             continue;
 
         PlotTextModule( textModule, getColor( textLayer ) );
diff --git a/pcbnew/tools/pcbnew_control.cpp b/pcbnew/tools/pcbnew_control.cpp
index 40f0859e2f..1691cb5ea5 100644
--- a/pcbnew/tools/pcbnew_control.cpp
+++ b/pcbnew/tools/pcbnew_control.cpp
@@ -651,9 +651,9 @@ void pasteModuleItemsToModEdit( MODULE* aClipModule, BOARD* aBoard,
             if( text->GetType() != TEXTE_MODULE::TEXT_is_DIVERS )
                 text->SetType( TEXTE_MODULE::TEXT_is_DIVERS );
 
-            if( text->GetText() == "%V" )
+            if( text->GetText() == "${VALUE}" )
                 text->SetText( aClipModule->GetValue() );
-            else if( text->GetText() == "%R" )
+            else if( text->GetText() == "${REFERENCE}" )
                 text->SetText( aClipModule->GetReference() );
 
             text->SetTextAngle( aClipModule->GetOrientation() );
diff --git a/qa/eeschema/lib_field_test_utils.h b/qa/eeschema/lib_field_test_utils.h
index 7bc85c2c64..1f578cd2ba 100644
--- a/qa/eeschema/lib_field_test_utils.h
+++ b/qa/eeschema/lib_field_test_utils.h
@@ -42,7 +42,7 @@ struct print_log_value<LIB_FIELD>
 {
     inline void operator()( std::ostream& os, LIB_FIELD const& f )
     {
-        os << "LIB_FIELD[ " << f.GetName( NATIVE_FIELD_NAME ) << " ]";
+        os << "LIB_FIELD[ " << f.GetCanonicalName() << " ]";
     }
 };
 
@@ -68,16 +68,15 @@ namespace KI_TEST
  * @param  aExpectedId the expected field id
  * @return           true if match
  */
-bool FieldNameIdMatches(
-        const LIB_FIELD& aField, const std::string& aExpectedName, int aExpectedId )
+bool FieldNameIdMatches( const LIB_FIELD& aField, const std::string& aExpectedName,
+                         int aExpectedId )
 {
     bool       ok = true;
-    const auto gotName = aField.GetName( false );
+    const auto gotName = aField.GetCanonicalName();
 
     if( gotName != aExpectedName )
     {
-        BOOST_TEST_INFO(
-                "Field name mismatch: got '" << gotName << "', expected '" << aExpectedName );
+        BOOST_TEST_INFO( "Field name: got '" << gotName << "', expected '" << aExpectedName );
         ok = false;
     }
 
@@ -85,7 +84,7 @@ bool FieldNameIdMatches(
 
     if( gotId != aExpectedId )
     {
-        BOOST_TEST_INFO( "Field ID mismatch: got '" << gotId << "', expected '" << aExpectedId );
+        BOOST_TEST_INFO( "Field ID: got '" << gotId << "', expected '" << aExpectedId );
         ok = false;
     }