Think.JS №04

  • Для начинающих: Объект Selection. Часть I
  • Интерфейс: Проблема выбора
  • Учим матчасть: … и дай попробовать
  • Полезный скрипт: Автоматический обновитель

Содержание

Для начинающих: Объект Selection. Часть I

Интерфейс: Проблема выбора

Учим матчасть: … и дай попробовать

Полезный скрипт: Автоматический обновитель

Для начинающих

Объект Selection. Часть I

Очень часто работа скрипта напрямую зависит от того, какие именно объекты выбрал пользователь. Например, если необходимо обработать текст, а пользователем была выделена картинка, то попытка обработать изображение как текст, приведет к завершению скрипта с ошибкой. Для того, чтобы точно знать, какой именно объект выделил пользователь в документе, нужно уметь обращаться с объектом Selection.

Что представляет собой данный объект? Прежде всего, он является не самостоятельным объектом, а свойством объекта app, поэтому обращаться к нему следует через конструкцию app.selection. Вызвав alert(app.selection.constructor.name) можно узнать, что объект является массивом, следовательно, имеет свойство length, которое указывает количество элементов в нем. Если скрипт требует наличия выделенного объекта для обработки, то, проверив это свойство, можно узнать, сколько объектов выделил пользователь.

Создадим новый документ с несколькими произвольными объектами и запустим нижеследующий скрипт.

selectionLength.jsx
Скрипт информирует о количестве выделенных объектов в документе
with (app) {
  if (selection.length == 0) {
    alert ("Ничего не выделено");
  } else {
    alert("Выделено " + String(selection.length) + " объект(ов)");
  }
}

Если количество не соответствует условиям скрипта, можно предупредить об этом пользователя и принудительно завершить работу. Следующий скрипт сообщает тип каждого из выделенных объектов в том случае, если selection.length больше нуля (вариант «меньше нуля» просто невозможен).

selectionTypes.jsx
Скрипт сообщает тип каждого выделенного объекта
with (app) {
  if (selection.length == 0) {
    alert ("Ничего не выделено");
    exit();
  } else {
    for (var counter = 0; counter < selection.length; counter++) {
      alert(selection[counter].constructor.name);
    }
  }
}

Если выделить несколько разнотипных объектов, а затем внимательно следить за сообщениями при выполнении скрипта, то можно проследить следующую закономерность: тот объект, который был выделен первым, будет обработан последним, и наоборот – тот, который был выбран последним, будет обработан первым. Из этого следует, что в Selection каждый новый объект добавляется не в конец массива, а в начало (как сказали бы «взрослые» программисты, реализует буфер LIFO: last input, first output – первым вошел, последним вышел). С практической точки зрения это означает, что всегда можно определить последовательность выбора пользователя. Это важно, например, в скрипте, который меняет размеры объектов по одному контрольному – для нахождения контрольного (в этом случае стоит заранее предупредить пользователя, какой объект будет считаться контрольным – первый или последний).

Отдельно следует сказать про команду exit(). Эта команда, вызванная в любом месте, завершает выполнение скрипта. Она является функцией JavaScript, а не методом объекта app, поэтому может вызываться вне блока with (app) {}.

В том, что объект Selection является массивом, есть соблазн попытаться выполнить selection.reverse() для того, чтобы «развернуть» объекты в массиве и обрабатывать от первого выделенного объекта к последнему. Но вызов этого метода, а также других методов обработки массивов (sort, push, pop, shift и т.д.) не оказывает ровно никакого влияния на Selection. Этот объект защищен от стандартных манипуляций. Чтобы обращаться с группой выделенных объектов как с настоящим массивом, нужно их передать в массив.

selectionArray.jsx
Скрипт демонстрирует возможность передачи selection в массив с последующей обработкой
with (app) {
  var mySel = [];
  if (selection.length == 0) {
    alert ("Ничего не выделено");
    exit();
  } else {
    for (var counter = 0; counter < selection.length; counter++) {
      mySel.push(selection[counter]);
    }
  }
  mySel.sort();
  alert(mySel);
}

Запустите этот скрипт, выделив несколько объектов разного типа. Метод массива push() добавляет объект, переданный в качестве параметра, в конец массива, поэтому массив mySel будет точно соответствовать по структуре объекту Selection. Функция сортировки массива sort() отсортирует объекты в зависимости от названий их типов по возрастанию.

Отдельным случаем объекта selection являются текстовые объекты, выделяемые при помощи Type Tool. Поскольку обработка выделенного текста является очень распространенной задачей для скриптов, эта тема требует подробного описания.

О том, какие команды в скрипте позволяют выделить нужные объекты – в части II. Но до этого мы изучим иерархию текстовых объектов, которую необходимо знать для правильной работы с выделенными пользователем объектами.

Интерфейс

Проблема выбора

Для предоставления пользователю возможности выбора из двух и более вариантов в диалоговых окнах InDesign предусмотрены виджеты checkboxControl, radiobuttonGroup и dropdown. Рассмотрим их свойства и методы применения более подробно.

Самым простым виджетом, позволяющим выбрать только из двух вариантов, является checkboxControl. В диалоговом окне он реализует флажок переключения, у которого есть два состояния – включен и выключен. У checkboxControl есть два полезных свойства: staticLabel и checkedState. Заметим, что такие же свойства присутствуют у enablingGroup, причем означают они то же самое. Тем самым облегчается запоминание свойств различных виджетов – если объект обладает статичной надписью, то это свойство называется staticLabel, если он может быть включен или выключен – checkedState.

Объект типа checkboxControl незаменим в любых случаях, когда нужно предоставить пользователю выбор вида «да – нет». Например, если скрипт выводит на печать страницы, в диалоговое окно можно добавить var useShowPrintDialog = dialogRows.add().checkboxControls.add({staticLabel: "Показывать диалог печати", checkedState: true}).

Модифицируем уже знакомый нам скрипт, в котором пользователь вводит данные сотрудника. Дело в том, что enablingGroup не всегда является удобным. Допустим, сотрудник пользуется e-mail, но не использует ICQ и Messenger, или не имеет доступа к почте и мессенджеру, а только к ICQ. Для уточнения доступности каждого средства связи правильнее использовать отдельный checkboxControl для каждой опции.

nameDialog.jsx
Скрипт для получения данных сотрудника (использование checboxControl)
with (app) {
  var myDialog = dialogs.add();
  with (myDialog.dialogColumns.add()) {
    with (dialogRows.add().borderPanels.add().dialogColumns.add()) {
      dialogRows.add().staticTexts.add({staticLabel: "Данные сотрудника"});
      with (dialogRows.add()) {
        with (dialogColumns.add()) {
          dialogRows.add().staticTexts.add({staticLabel: "Фамилия:"});
          dialogRows.add().staticTexts.add({staticLabel: "Имя:"});
          dialogRows.add().staticTexts.add({staticLabel: "Отчество:"});
        }
        with (dialogColumns.add()) {
         var useFam = dialogRows.add().textEditboxes.add({editContents: "Иванов"});
         var useName = dialogRows.add().textEditboxes.add({editContents: "Иван"});
         var useName2 = dialogRows.add().textEditboxes.add({editContents: "Иванович"});
        }
      }
    }
    with (dialogRows.add().borderPanels.add().dialogColumns.add()) {
      dialogRows.add().staticTexts.add({staticLabel: "Доступные средства связи:"});
      with (dialogRows.add()) {
        with (dialogColumns.add()) {
          var useHasEmail = dialogRows.add().checkboxControls.add({staticLabel: "E-mail:", checkedState: false});
          var useHasICQ = dialogRows.add().checkboxControls.add({staticLabel: "ICQ:", checkedState: false});
          var useHasMessenger =dialogRows.add().checkboxControls.add({staticLabel: "Messenger:", checkedState: false});
        }
        with (dialogColumns.add()) {
          var useEmail = dialogRows.add().textEditboxes.add({editContents: ""});
          var useICQ = dialogRows.add().textEditboxes.add({editContents: ""});
          var useMessenger = dialogRows.add().textEditboxes.add({editContents: ""});
        }
      }
    }
  }
  myDialog.show();
}

Проверка доступности конкретного средства связи может быть осуществлена следующим образом:

checkEmail
Проверка checkboxControl (является частью скрипта nameDialog.jsx)
if (useHasEmail.checkedState) {
  myEmail = useEmail.editContents;
} else {
 myEmail = "Почтовый адрес недоступен"; 
}

Заметим, что в этом случае даже, если пользователь ввел значение в текстовое поле, но не включил соответствующий чекбокс, то введенное значение будет проигнорировано. Правильно это или нет – зависит от конкретных условий. Допустим, если сотрудник имеет почтовый адрес, но он по каким-то причинам не должен быть «обнародован» в результате работы скрипта, то применение чекбокса более чем оправдано, поскольку позволяет ввести адрес и скрыть его. Вне зависимости от того, включил ли пользователь чекбокс или нет, свойства поля ввода остаются доступными, данные из них считать все равно можно. Но если нет нужды скрывать данные, то можно просто проверять textEditbox.editContents – если значением этого свойства является пустая строка: textEditbox.editContents == “”, то пользователь не ввел никакого значения (если ввел, то правильность введенного значения можно проконтролировать, но это – отдельная и достаточно сложная тема).

Виджет radiobuttonGroup реализует в диалоговом окне группу из нескольких радиокнопок. Только одна из радиокнопок может быть включена в один момент времени – при включении другой кнопки, та, что была включена, автоматически выключается.

Сама по себе группа радиокнопок никак не отображается в диалоговом окне – она должна содержать как минимум один объект типа radiobuttonControl (коллекция radiobuttonGroup.radiobuttonControls по умолчанию не содержит ни одного объекта).

nameDialog.jsx
Скрипт для получения данных сотрудника (использование radiobuttonGroup)
with (app) {
  var myDialog = dialogs.add();
  with (myDialog.dialogColumns.add()) {
    with (dialogRows.add().borderPanels.add().dialogColumns.add()) {
      dialogRows.add().staticTexts.add({staticLabel: "Данные сотрудника"});
      with (dialogRows.add()) {
        with (dialogColumns.add()) {
          dialogRows.add().staticTexts.add({staticLabel: "Фамилия:"});
          dialogRows.add().staticTexts.add({staticLabel: "Имя:"});
          dialogRows.add().staticTexts.add({staticLabel: "Отчество:"});
        }
        with (dialogColumns.add()) {
         var useFam = dialogRows.add().textEditboxes.add({editContents: "Иванов"});
         var useName = dialogRows.add().textEditboxes.add({editContents: "Иван"});
         var useName2 = dialogRows.add().textEditboxes.add({editContents: "Иванович"});
        }
      }
      var useDepartment = dialogRows.add().radiobuttonGroups.add();
      with (useDepartment) {
        radiobuttonControls.add({staticLabel: "Бухгалтерия", checkedState: true});
        radiobuttonControls.add({staticLabel: "Редакция"});
        radiobuttonControls.add({staticLabel: "Печатный цех"});
      }
    }
  }
  myDialog.show();
}

Радиокнопки (radiobuttonControls) добавляются без указания dialogRows.add(). Свойства радиокнопки – уже знакомые нам staticLabel и checkedState. Заметим, что если каждому новому контролу указывать checkedState: true, то в предыдущих это свойство автоматически выключается. Именовать радиокнопки можно, но вовсе не обязательно, поскольку узнать, какая именно радиокнопка выбрана можно при помощи свойства группы радиокнопок radiobuttonGroup.selectedButton. Это свойство поддерживает и чтение и запись, поэтому после формировании группы (но до отображения диалогового окна) можно указать, какая радиокнопка должна быть включенной. Нумерация, как и в любом другом массиве, начинается с нуля, поэтому первая радиокнопка имеет индекс 0, вторая – 1 и т.д.

Виджет dropdown реализует в диалоговом окне так называемый выпадающий список. Обязательным свойством списка является stringList – массив строк, каждая из которых будет отдельным элементом списка. Свойство selectedIndex определяет индекс элемента, который будет выбран в списке. Это свойство может быть указано до отображения диалогового окна и считано после того, как пользователь выполнил настройки и закрыл окно. Нумерация, как всегда, начинается с нуля. Если не указать selectedIndex до отображения диалогового окна, то значением это свойства будет -1, а отображаться в окне списка будет пустая строка (пользователь выбрать пустую строку не может). Для того, чтобы в окне списка отображался существующий элемент, следует указать значение (любое целое значение, желательно в диапазоне 0 – stringList.length -1, чтобы не путаться)

nameDialog.jsx
Скрипт для получения данных сотрудника (использование dropdowns)
with (app) {
  var listDepartment = ["Бухгалтерия", "Редакция", "Печатный цех"];
  var myDialog = dialogs.add();
  with (myDialog.dialogColumns.add()) {
    with (dialogRows.add().borderPanels.add().dialogColumns.add()) {
      dialogRows.add().staticTexts.add({staticLabel: "Данные сотрудника"});
      with (dialogRows.add()) {
        with (dialogColumns.add()) {
          dialogRows.add().staticTexts.add({staticLabel: "Фамилия:"});
          dialogRows.add().staticTexts.add({staticLabel: "Имя:"});
          dialogRows.add().staticTexts.add({staticLabel: "Отчество:"});
        }
        with (dialogColumns.add()) {
         var useFam = dialogRows.add().textEditboxes.add({editContents: "Иванов"});
         var useName = dialogRows.add().textEditboxes.add({editContents: "Иван"});
         var useName2 = dialogRows.add().textEditboxes.add({editContents: "Иванович"});
        }
      }
      dialogRows.add().staticTexts.add({staticLabel: "Отдел:"});
      var useDepartment = dialogRows.add().dropdowns.add({stringList: listDepartment, selectedIndex: 0});
    }
  }
  myDialog.show();
}

Очевидно, что dropdown и enablingGroup обладают схожей функцией – позволяют выбирать одно значение их списка. Я лично предпочитаю dropdown по нескольким причинам. Во-первых, это более компактный виджет, что при невозможности полноценного контроля за размерами очень полезно. Во-вторых, в отличие от группы радиокнопок список имеет размеры, совпадающие с размерами других виджетов (промежутки между радиокнопками не равны промежуткам между отдельными виджетами других типов, что делает невозможным выравнивание). В-третьих, использование списка делает код более простым и понятным.

Учим матчасть

… и дай попробовать

Каким образом можно повысить надежность скрипта? В первую очередь, конечно, писать правильный, понятный и хорошо структурированный код. Но даже идеальный код не поможет, если из-за ошибки пользователя скрипт потенциально может аварийно завершиться. Например, если скрипт обрабатывает только текстовые фреймы, а пользователь случайно выделил группу (не заметив, что текстовый фрейм сгруппирован с прямоугольником), то попытка обработать группу приведет к неприятным последствиям. Для того чтобы этого не случилось, нужно уметь правильно пользоваться управлением исключениями.

Управление исключениями в JavaScript осуществляется при помощи конструкции try {} catch (err) {} finally{}. Сначала выполняется последовательность операторов в фигурных скобках try{}
(try – пробовать); если во время выполнения возникли ошибки, то дальнейшее выполнение операторов в try{} прекращается и начинается выполнение блока операторов в catch(){}
(catch – ловить); если же при выполнении блока операторов try{} ошибок не возникло, то выполняется finally{}. Надо сказать, что finally{} используется достаточно редко – в том случае, если ошибки не возникло, скрипт просто выполняется дальше.

Иногда бывает очень полезно проверить наличие некоего свойства у обрабатываемого объекта. Например, если в InDesign не открыто ни одного документа, то свойство app.activeDocument, не будет определено, следовательно, попытка обратиться к нему вызовет ошибку.

checkDoc.jsx
Процедура проверки activeDocument
with (app) {
  try {
    var myDoc = activeDocument;
  } catch (error) {
    alert("Нет открытых документов!");
    exit();
  }
}

Можно, конечно, сначала проверить app.documents.length > 0, затем присвоить myDoc = documents[0]. Но это уже две операции вместо одной – присваивания.

Многие объекты InDesign имеют сходные свойства – например, все объекты коллекции pageItems
(oval, rectangle, textFrame и т.д.) имеют свойство geometricBounds. Следовательно, проверив, определено ли это свойство для выбранного пользователем объекта, можно узнать, есть возможность работы с этим объектом как с pageItem.

checkPageItem.jsx
Скрипт проверяет, является ли выбранный пользователем объект pageItem
with (app) {
  try {
    var myDoc = activeDocument;
  } catch (error) {
    alert("Нет открытых документов");
    exit();
  }
  try {
    var myPageItem = selection[0];
    var chkBounds = myPageItem.geometricBounds;
  } catch (error) {
    alert("Не выделен объект");
    exit();
  }
}

Точно также можно определить, является ли выбранный объект текстовым. Дело в том, что все текстовые объекты имеют свойство parentStory. Если нужно обработать весь текст story, может быть применен следующий скрипт.

checkText.jsx
Скрипт проверяет, является ли выбранный пользователем объект текстовым
with (app) {
  try {
    var myDoc = activeDocument;
  } catch (error) {
    alert("Нет открытых документов");
    exit();
  }
  try {
    var myTextObj = selection[0];
    var myStory = myTextObj.parentStory;
  } catch (error) {
    alert("Не выделен текст");
    exit();
  }
}

Конструкции try{} catch(){} могут быть вложенными. В блоке catch(){} может содержаться еще одна такая конструкция, в ней – следующая и т.д. Но не следует делать очень много вложений – код становится менее понятным.

Скрипт для определения текстового объекта не может распознать таблицу как тестовый объект, поскольку table не имеет свойства parentStory. Это не приведет к ошибке, но пользователь может и не понять логику скрипта. Зато свойство table.parent обычно указывает на textFrame, у которого есть свойство parentStory. Этим можно воспользоваться, сделав тройное вложение (третье – на случай, если пользователь выбрал не всю таблицу, а только ячейку).

checkText.jsx
Скрипт проверяет, является ли выбранный пользователем объект текстовым (с тройной проверкой)
with (app) {
  try {
    var myDoc = activeDocument;
  } catch (error) {
    alert("Нет открытых документов");
    exit();
  }
  try {
    var myTextObj = selection[0];
    var myStory = myTextObj.parentStory;
  } catch (error) {
    try {
      var myStory = myTextObj.parent.parentStory;
    } catch (error) {
      try {
        var myStory = myTextObj.parent.parent.parentStory;
      } catch (error) {
        alert("Не выделен текст");
        exit();
      }
    }
  }
}

Если нужно обработать только объект, который выбрал пользователь, то можно просто проверить свойство contents, характерное исключительно для текстовых объектов.

checkText.jsx
Скрипт проверяет, является ли выбранный пользователем объект текстовым (без нахождения parentStory)
with (app) {
  try {
    var myDoc = activeDocument;
  } catch (error) {
    alert("Нет открытых документов");
    exit();
  }
  try {
    var myTextObj = selection[0];
    var chkContents = myTextObj.contents;
  } catch (error) {
    alert("Не выбран текстовый объект");
    exit();
  }
}

Конструкция try{} catch (){} незаменима при отладке скриптов. Об этом – в следующий раз.

Полезный скрипт

Автоматический обновитель

Обычно я обрабатываю фотографии в PhotoShop большими комплектами – не менее пяти штук за раз. После обработки приходилось в InDesign переключаться в панель Links и вручную обновлять все связи. Потеря времени небольшая, но все равно никчемная, поэтому не выдержал и написал скрипт, который обновляет все изменившиеся связанные файлы, и присвоил ему клавиши быстрого доступа. Как говорится, must have.

linkUpdate.jsx
Скрипт для обновления связанных графических файлов
function collectGraphics (myArray) {
  var myResult = [];
  for (var counter = 0; counter < myArray.length; counter++) {
    try {
      myResult = myResult.concat(myArray[counter].allGraphics);
    } catch (error) {
      continue;
    }
  }
  return myResult;
}
function updateLink (myArray) {
  for (var counter = 0; counter < myArray.length; counter++) {
    try {
      var myLink = myArray[counter].itemLink;
      if (myLink.status == LinkStatus.linkOutOfDate) {
        myLink.update();
      }
    } catch (error) {
      continue;
    }
  }
}
with (app) {
  try {
    var myDoc = activeDocument
  } catch (error) {
    alert("Нет открытых документов!");
    exit();
  }
  switch (selection.length) {
    case 0:
      var myGraphics = myDoc.allGraphics;
      break;
    default:
      var myGraphics = collectGraphics(selection);
      break;
  }
  updateLink(myGraphics);
}

Скрипт имеет две функции – collectGraphics и updateLink. Первая анализирует переданный в качестве параметра массив и возвращает все объекты типа allGraphics. Вторая – проверяет статус, если требуется обновление, то связь обновляется.

В зависимости от того, выделил ли пользователь объекты в документе, скрипт обрабатывает только выделенные объекты (собрав их в массив при помощи collectGraphics) или все связанные графические файлы.


Олег Бутрин
THINK.JS выпуск № 4 от 2006-11-20

 

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s