chromium/tools/grit/grit/tool/resize.py

# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

'''The 'grit resize' tool.
'''


import getopt
import os
import sys

from grit import grd_reader
from grit import pseudo
from grit import util
from grit.format import rc
from grit.format import rc_header
from grit.node import include
from grit.tool import interface


# Template for the .vcproj file, with a couple of [[REPLACEABLE]] parts.
PROJECT_TEMPLATE = '''\
<?xml version="1.0" encoding="Windows-1252"?>
<VisualStudioProject
  ProjectType="Visual C++"
  Version="7.10"
  Name="[[DIALOG_NAME]]"
  ProjectGUID="[[PROJECT_GUID]]"
  Keyword="Win32Proj">
  <Platforms>
    <Platform
      Name="Win32"/>
  </Platforms>
  <Configurations>
    <Configuration
      Name="Debug|Win32"
      OutputDirectory="Debug"
      IntermediateDirectory="Debug"
      ConfigurationType="1"
      CharacterSet="2">
    </Configuration>
  </Configurations>
  <References>
  </References>
  <Files>
    <Filter
      Name="Resource Files"
      Filter="rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx"
      UniqueIdentifier="{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}">
      <File
        RelativePath=".\\[[DIALOG_NAME]].rc">
      </File>
    </Filter>
  </Files>
  <Globals>
  </Globals>
</VisualStudioProject>'''


# Template for the .rc file with a couple of [[REPLACEABLE]] parts.
# TODO(joi) Improve this (and the resource.h template) to allow saving and then
# reopening of the RC file in Visual Studio.  Currently you can only open it
# once and change it, then after you close it you won't be able to reopen it.
RC_TEMPLATE = '''\
// This file is automatically generated by GRIT and intended for editing
// the layout of the dialogs contained in it.  Do not edit anything but the
// dialogs.  Any changes made to translateable portions of the dialogs will
// be ignored by GRIT.

#include "resource.h"
#include <winresrc.h>
#ifdef IDC_STATIC
#undef IDC_STATIC
#endif
#define IDC_STATIC (-1)

LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL

#pragma code_page([[CODEPAGE_NUM]])

[[INCLUDES]]

[[DIALOGS]]
'''


# Template for the resource.h file with a couple of [[REPLACEABLE]] parts.
HEADER_TEMPLATE = '''\
// This file is automatically generated by GRIT.  Do not edit.

#pragma once

// Edit commands
#define ID_EDIT_CLEAR                   0xE120
#define ID_EDIT_CLEAR_ALL               0xE121
#define ID_EDIT_COPY                    0xE122
#define ID_EDIT_CUT                     0xE123
#define ID_EDIT_FIND                    0xE124
#define ID_EDIT_PASTE                   0xE125
#define ID_EDIT_PASTE_LINK              0xE126
#define ID_EDIT_PASTE_SPECIAL           0xE127
#define ID_EDIT_REPEAT                  0xE128
#define ID_EDIT_REPLACE                 0xE129
#define ID_EDIT_SELECT_ALL              0xE12A
#define ID_EDIT_UNDO                    0xE12B
#define ID_EDIT_REDO                    0xE12C


[[DEFINES]]
'''


class ResizeDialog(interface.Tool):
  '''Generates an RC file, header and Visual Studio project that you can use
with Visual Studio's GUI resource editor to modify the layout of dialogs for
the language of your choice.  You then use the RC file, after you resize the
dialog, for the language or languages of your choice, using the <skeleton> child
of the <structure> node for the dialog.  The translateable bits of the dialog
will be ignored when you use the <skeleton> node (GRIT will instead use the
translateable bits from the original dialog) but the layout changes you make
will be used.  Note that your layout changes must preserve the order of the
translateable elements in the RC file.

Usage: grit resize [-f BASEFOLDER] [-l LANG] [-e RCENCODING] DIALOGID*

Arguments:
  DIALOGID        The 'name' attribute of a dialog to output for resizing.  Zero
                  or more of these parameters can be used.  If none are
                  specified, all dialogs from the input .grd file are output.

Options:

  -f BASEFOLDER   The project will be created in a subfolder of BASEFOLDER.
                  The name of the subfolder will be the first DIALOGID you
                  specify.  Defaults to '.'

  -l LANG         Specifies that the RC file should contain a dialog translated
                  into the language LANG.  The default is a cp1252-representable
                  pseudotranslation, because Visual Studio's GUI RC editor only
                  supports single-byte encodings.

  -c CODEPAGE     Code page number to indicate to the RC compiler the encoding
                  of the RC file, default is something reasonable for the
                  language you selected (but this does not work for every single
                  language).  See details on codepages below.  NOTE that you do
                  not need to specify the codepage unless the tool complains
                  that it's not sure which codepage to use.  See the following
                  page for codepage numbers supported by Windows:
                  http://www.microsoft.com/globaldev/reference/wincp.mspx

  -D NAME[=VAL]   Specify a C-preprocessor-like define NAME with optional
                  value VAL (defaults to 1) which will be used to control
                  conditional inclusion of resources.


IMPORTANT NOTE:  For now, the tool outputs a UTF-8 encoded file for any language
that can not be represented in cp1252 (i.e. anything other than Western
European languages).  You will need to open this file in a text editor and
save it using the codepage indicated in the #pragma code_page(XXXX) command
near the top of the file, before you open it in Visual Studio.

'''

  # TODO(joi) It would be cool to have this tool note the Perforce revision
  # of the original RC file somewhere, such that the <skeleton> node could warn
  # if the original RC file gets updated without the skeleton file being updated.

  # TODO(joi) Would be cool to have option to add the files to Perforce

  def __init__(self):
    self.lang = pseudo.PSEUDO_LANG
    self.defines = {}
    self.base_folder = '.'
    self.codepage_number = 1252
    self.codepage_number_specified_explicitly = False

  def SetLanguage(self, lang):
    '''Sets the language code to output things in.
    '''
    self.lang = lang
    if not self.codepage_number_specified_explicitly:
      self.codepage_number = util.LanguageToCodepage(lang)

  def GetEncoding(self):
    if self.codepage_number == 1200:
      return 'utf_16'
    if self.codepage_number == 65001:
      return 'utf_8'
    return 'cp%d' % self.codepage_number

  def ShortDescription(self):
    return 'Generate a file where you can resize a given dialog.'

  def Run(self, opts, args):
    self.SetOptions(opts)

    own_opts, args = getopt.getopt(args, 'l:f:c:D:', ('help',))
    for key, val in own_opts:
      if key == '-l':
        self.SetLanguage(val)
      if key == '-f':
        self.base_folder = val
      if key == '-c':
        self.codepage_number = int(val)
        self.codepage_number_specified_explicitly = True
      if key == '-D':
        name, val = util.ParseDefine(val)
        self.defines[name] = val
      elif key == '--help':
        self.ShowUsage()
        sys.exit(0)

    res_tree = grd_reader.Parse(opts.input, debug=opts.extra_verbose)
    res_tree.OnlyTheseTranslations([self.lang])
    res_tree.RunGatherers()

    # Dialog IDs are either explicitly listed, or we output all dialogs from the
    # .grd file
    dialog_ids = args
    if not len(dialog_ids):
      for node in res_tree:
        if node.name == 'structure' and node.attrs['type'] == 'dialog':
          dialog_ids.append(node.attrs['name'])

    self.Process(res_tree, dialog_ids)

  def Process(self, grd, dialog_ids):
    '''Outputs an RC file and header file for the dialog 'dialog_id' stored in
    resource tree 'grd', to self.base_folder, as discussed in this class's
    documentation.

    Arguments:
      grd: grd = grd_reader.Parse(...); grd.RunGatherers()
      dialog_ids: ['IDD_MYDIALOG', 'IDD_OTHERDIALOG']
    '''
    grd.SetOutputLanguage(self.lang)
    grd.SetDefines(self.defines)

    project_name = dialog_ids[0]

    dir_path = os.path.join(self.base_folder, project_name)
    if not os.path.isdir(dir_path):
      os.mkdir(dir_path)

    # If this fails then we're not on Windows (or you don't have the required
    # win32all Python libraries installed), so what are you doing mucking
    # about with RC files anyway? :)
    # pylint: disable=import-error
    import pythoncom

    # Create the .vcproj file
    project_text = PROJECT_TEMPLATE.replace(
      '[[PROJECT_GUID]]', str(pythoncom.CreateGuid())
      ).replace('[[DIALOG_NAME]]', project_name)
    fname = os.path.join(dir_path, '%s.vcproj' % project_name)
    self.WriteFile(fname, project_text)
    print("Wrote %s" % fname)

    # Create the .rc file
    # Output all <include> nodes since the dialogs might depend on them (e.g.
    # for icons and bitmaps).
    include_items = []
    for node in grd.ActiveDescendants():
      if isinstance(node, include.IncludeNode):
        include_items.append(rc.FormatInclude(node, self.lang, '.'))
    rc_text = RC_TEMPLATE.replace('[[CODEPAGE_NUM]]',
                                  str(self.codepage_number))
    rc_text = rc_text.replace('[[INCLUDES]]', ''.join(include_items))

    # Then output the dialogs we have been asked to output.
    dialogs = []
    for dialog_id in dialog_ids:
      node = grd.GetNodeById(dialog_id)
      assert node.name == 'structure' and node.attrs['type'] == 'dialog'
      # TODO(joi) Add exception handling for better error reporting
      dialogs.append(rc.FormatStructure(node, self.lang, '.'))
    rc_text = rc_text.replace('[[DIALOGS]]', ''.join(dialogs))

    fname = os.path.join(dir_path, '%s.rc' % project_name)
    self.WriteFile(fname, rc_text, self.GetEncoding())
    print("Wrote %s" % fname)

    # Create the resource.h file
    header_defines = ''.join(rc_header.FormatDefines(grd))
    header_text = HEADER_TEMPLATE.replace('[[DEFINES]]', header_defines)
    fname = os.path.join(dir_path, 'resource.h')
    self.WriteFile(fname, header_text)
    print("Wrote %s" % fname)

  def WriteFile(self, filename, contents, encoding='cp1252'):
    with open(filename, 'wb') as f:
      writer = util.WrapOutputStream(f, encoding)
      writer.write(contents)