Smalltalk по-русски
Advertisement

Что такое 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 sequence view.gif

Украшательство можно продолжить, например, зачеркнуть выполненные задачи:

 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: [#()]

Суть изменения ясна из картинки:

Trippy class comment.gif

Любопытно, что почти тот же код имеется в методе 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 due date nil.gif

Приятная фича 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

Trippy custom action in menu.gif

Ещё одна полезная штука: меняя метод 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:

Trippy custom collaborators in menu.gif

Панели инспекторов[]

У большинства объектов окно инспектора содержит две закладки: 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 присутствует также и во всплывающем меню:

Trippy related tasks pane.gif

Инспектор можно представлять себе как средство навигации по иерархии частей, из которых состоят объекты. Понимание этого дает ключ к нескольким очень полезным механизмам, ускоряющим перемещение по иерархии: Explore parts, Explore siblings, Explore visited. Вот, например, что делает Explore parts с нашим RelatedTasksInspector

Trippy related tasks pane explore parts.gif

Создание собственных 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 сохраняется и при этом даже обновляется в других инспекторах.

Ссылки[]

Добавляем собственные установки

Расширение "Инспектора"

Advertisement