Что такое Trippy и зачем о нем знать[]
В некоторый момент в VisualWorks место традиционного инспектора объектов занял Trippy -- framework, позволяющий создавать специализированные инспекторы для различных классов. Вот несколько примеров таких инспекторов:
- SequenceInspector (для упорядоченных коллекций)
- SetInspector и DictionaryInspector
- TextEditorInspector -- для String и Text
- BehaviorInspector -- браузер, встроенный в инспектор (вкладка Methods инспектора)
В Cincom сейчас идет работа над превращением в аналогичный framework браузера, что позволит на его базе создавать браузеры для любых доменов (а не только для классов Смолтока). Аналогичная работа ведется и для Squeak -- в рамках проекта OmniBrowser.
Знать и уметь Trippy полезно потому, что с его помощью можно за минимальное время (минуты) создавать дополнительные удобства, облегчающие процесс разработки. Это интересная и поучительная альтернатива созданию "настоящего" графического интерфейса (деятельность, заслуженно считающаяся "могилой человеко-часов").
В этом тьюториале показывается, как "заточить" стандартный инспектор Trippy под нужды игрушечного домена.
Стоит также прочитать описание Trippy (по-английски): http://www.cincomsmalltalk.com/CincomSmalltalkWiki/Trippy+(new+inspector)+Walkthrough
Где берется обсуждаемый здесь код?[]
Выложен в Cincom Public Repository, пакет называется TaskTrippyTutorial.
Было бы неплохо, если бы кто-нибудь захостил парсель, чтобы его могли забрать те, у кого нет доступа к репозиторию, а есть только HTTP
Наш игрушечный домен[]
Класс Task -- упрощенная версия того, что могло бы появиться в программе-органайзере или в системе учета проблем.
- <text> - описание задачи; первая (или единственная) строка будет использоваться при отображении задачи в списках (см. метод Task >> title)
- <complete> - булевский признак того, что задача завершена
- <dueDate> - срок, к которому должна быть завершена задача (в виде Date), либо nil
- <relations> - отношения, в которые входит данная задача
В нашем примере эти отношения сведены к единственному бинарному отношению BasicTaskLink. Каждая из двух задач, входящих в это отношение, имеет одну из двух ролей, незамысловато названых first и second. Имея на руках задачу, можно попросить у связи имя роли для другого конца связи (метод otherRoleNameFor:) или объект на другом конце связи (метод otherFor:).
Строковые представления объектов[]
Для начала стоит разобраться с представлением объектов в виде строк. Обычно объекты пользовательских типов в отладчике и при печати на Workspace выглядят как нечто вроде "a Tools.Trippy.TaskTutorial.Task". Не очень информативно, но совсем нетрудно сделать так, чтобы в печатное представление попадало название задачи.
Task >> printOn: aStream aStream nextPutAll: 'Task('; nextPutAll: self title; nextPut: $)
Теперь значение выражения вроде Task text: 'Вымыть окна' выглядело при печати на Workspace как Task('вымыть окна').
Иногда удобнее переопределять метод printString, который по умолчанию вызывает printOn:
Task >> printString ^'Task(' , self title, ')'
Далее, существует специальный метод для получения строкового представления, которое будет использоваться в GUI (прежде всего в SequenceView). По умолчанию этот метод используется printString
Task >> displayString ^self title
Чтобы увидеть, как выглядит новоиспеченное строковое представление для GUI, можно сделать нечто вроде
SequenceView openOn: (List with: (Task text: 'clean up windows') with: (Task text: 'repair the iron'))
Малоизвестный, но полезный факт: displayString может вернуть не только строку, но и Text (восходящий к седой древности класс для представления текста с форматированием).
Task >> displayString | textRepresentation | textRepresentation := self text asText. self complete ifFalse: [textRepresentation allBold]. ^textRepresentation
Украшательство можно продолжить, например, зачеркнуть выполненные задачи:
Task >> displayString ^self text asText emphasizeAllWith: (self complete ifTrue: [#strikeout] ifFalse: [#bold])
На "украшенную" версию можно полюбоваться с помощью
SequenceView openOn: (List with: ((Task text: 'clean up windows') complete: true) with: (Task text: 'repair the iron'))
Простейшие способы расширения Trippy[]
Начнем с милого пустяка:
Object >> inspectorExtraAttributes ^self class comment isEmpty ifFalse: [Array with: (Tools.Trippy.TextAttribute label: 'class comment' text: self class comment)] ifTrue: [#()]
Суть изменения ясна из картинки:
Любопытно, что почти тот же код имеется в методе inspectorExtraAttributes у стандартного ClassDescription (но мне нравится возможность посмотреть комментарии, не отрываясь от инстанса):
ClassDescription >> inspectorExtraAttributes ^Array with: (Tools.Trippy.TextAttribute label: (#comment << #dialogs >> 'comment') textBlock: [self comment])
Обратите внимание, что используется конструктор label:textBlock:, а не label:text:. Использование textBlock позволяет пересчитывать текст при каждом обновлении окна инспектора.
Попробуем, вооружившись приобретенными знаниями, изготовить дополнительный атрибут. Пусть это будет число дней, оставшихся до намеченного срока завершения:
Task >> inspectorExtraAttributes ^super inspectorExtraAttributes copyWith: (Tools.Trippy.TextAttribute label: 'days due' textBlock: [| daysDue | dueDate ifNil: ['n/a'] ifNotNil: [daysDue := dueDate subtractDate: Date today. daysDue printString]])
Приятная фича Trippy: чтобы поменять значение переменной, достаточно выделить её, ввести на правой панели новое значение и нажать Accept (Control-S). Например, если ввести Date today, значение вычисленного атрибута days due сменится на 0. Доступ к переменной происходит, естественно, минуя методы (если таковые имеются) через мета-протокол
Вот еще пример того, для чего пригождаются дополнительные атрибуты. Есть такая хорошая штука -- кватернионы. Отличная вещь для представления поворотов в трехмерном пространстве и для интерполяции при анимации, но вот беда: четыре параметра кватерниона почти ничего не говорят человеку. Зато человек неплохо понимает повороты в виде Эйлеровых углов. Ничто не мешает добавить кватернионам дополнительный атрибут, показывающий соответствующие ему углы Эйлера. Например, для кватерниона <3.1415 0.7071 0.7071 0> это будут (в градусах) крен -180, тангаж 0, рысканье -90.
Продолжим нащи игры.
Task >> inspectorActions ^Array with: (Action label: 'Mark as done' block: [self complete: true])
Дополнительные действия над объектом попадают в меню Object
Ещё одна полезная штука: меняя метод inspectorCollaborators мы можем определять объекты, тесно сотрудничающие с данным и добавлять команды для перехода к ним в меню Go.
В нашем крошечном домене пример будет довольно наиграным. Хорошим примером использования этого механизма Trippy является класс ApplicationWindow. Мы же в качестве "сотрудника" будем рассматривать коллекцию задач, связанных с данной.
Task >> inspectorCollaborators ^Array with: (Tools.Trippy.Collaborator label: 'Related Tasks' block: [self relations collect: [:each | each otherFor: self]])
попробуем создать несколько связанных между собой задач:
t1 := Task text: 'clean up the windows'. t2 := Task text: 'buy glass cleaner'. t3 := Task text: 'fix the ladder'. l1 := BasicTaskLink first: t1 second: t2. l2 := BasicTaskLink first: t1 second: t3. t1 inspect
вот как выглядит теперь меню Go:
Панели инспекторов[]
У большинства объектов окно инспектора содержит две закладки: Basic и Methods. У коллекций добавляется третья -- Elements. Мы можем изготовить свою собственную закладку -- например, у задачи может быть закладка Related Tasks.
Каждой закладке соответствует класс инспектора. Перечень классов задается методов inspectorClasses:
Task >> inspectorClasses ^super inspectorClasses copyWith: RelatedTasksInspector
Класс BehaviorInspector (закладка Methods) добавляется в конец всегда.
RelatedTasksInspector -- это очень простой инспектор, в нем переопределяются только два метода, унаследованных от предка, BasicInspector. Эти два метода образуют протокол deсomposition, и описывают разделение исследуемого объекта на абстрактные части (parts). Части -- это очень фундаментальное понятие в Trippy. Частями являются переменные экземпляра, элементы коллекций, дополнительные атрибуты. Все части относятся к классам, производным от Part. В RelatedTasksInspector мы используем стандартную часть для вычислимых атрибутов -- DerivedAttribute.
RelatedTasksInspector >> partCount ^self relations size
RelatedTasksInspector >> partAt: anInteger | r | r := self relations at: anInteger. ^Tools.Trippy.DerivedAttribute label: (r otherRoleNameFor: object) value: (r otherFor: object)
Используемый в этих двух методах self relations просто кеширует коллецию отношений, возвращаемую объектом Task:
RelatedTasksInspector >> relations ^relations ifNil: [relations := object relations]
Кроме того, на стороне класса у RelatedTaskInspector переопределен метод tabLabel. Вот и всё!
Вот что получается в результате. Обратите внимание, что реализованное нами ранее дополнительное действие Mark as done присутствует также и во всплывающем меню:
Инспектор можно представлять себе как средство навигации по иерархии частей, из которых состоят объекты. Понимание этого дает ключ к нескольким очень полезным механизмам, ускоряющим перемещение по иерархии: Explore parts, Explore siblings, Explore visited. Вот, например, что делает Explore parts с нашим RelatedTasksInspector
Создание собственных Parts[]
Попробуем теперь создать собственную часть, которая будет "знать" про то, как устроены связи между задачами. Для этого мы расширим наш RelatedTasksInspector до AdvancedRelatedTasksInspector
Task >> inspectorClasses ^super inspectorClasses copyWith: AdvancedRelatedTasksInspector
Протокол decomposition выглядит несколько иначе:
AdvancedRelatedTasksInspector >> partAt: anInteger | r | r := self relations at: anInteger. ^TaskLinkPart forLink: r task: object
Вновь создаваемая TaskLinkPart в качестве значения будет возвращать противоположный конец связи
TaskLinkPart >> value ^self link otherFor: self task
При этом в списке частей TaskLinkPart будет показывать роль, соответствующую противоположному концу связи
TaskLinkPart >> displayString ^self link otherRoleNameFor: self task
Расширение меню[]
Иногда действия связаны не с конкретным объектом, а с группой объектов. Например, мы хотим посмотреть все задачи, связанные с выделенными в данный момент. Для этого можно создать всплывающее меню для списка частей:
AdvancedRelatedTasksInspector >> buildFieldListMenu ^(super buildFieldListMenu) addLine; addItem: ((MenuItem labeled: 'Inspect linked to selected') nameKey: #inspectLinkedToSelected; value: [self inspectLinkedToSelected]; enabled: [self selections size >= 1]); yourself
Кроме того, метод augmentMenuBar: дает зацепку для добавления собственных пунктов в меню окна инспектора.
Прочее[]
Возможно, вы этого не знали, но Trippy поддерживает Drag and Drop. В простых случаях от инспектора достаточно обработать сообщения drop:at: и drop:before:, а также, возможно, переопределить dragControllerClass. Посмотрите, как это сделано в AdvancedRelatedTasksInspector
Кстати, части из инспектора можно таскать на Workspace для дальнейшего исследования.
У инспектора, кстати, есть свой собственный маленький Workspace -- Evaluation pane (которая по умолчаию спрятана). Содержимое этого Workspace сохраняется и при этом даже обновляется в других инспекторах.