"""A simple Mac-only browse utility to peek at the inner data structures of Python."""
# Minor modifications by Jack to facilitate incorporation in twit.

# june 1996
# Written by Just van Rossum <just@knoware.nl>, please send comments/improvements.
# Loosely based on Jack Jansens's PICTbrowse.py, but depends on his fabulous FrameWork.py
# XXX Some parts are *very* poorly solved. Will fix. Guido has to check if all the
# XXX "python-peeking" is done correctly. I kindof reverse-engineered it ;-)

# disclaimer: although I happen to be the brother of Python's father, programming is
# not what I've been trained to do. So don't be surprised if you find anything that's not 
# as nice as it could be...

# XXX to do:
# Arrow key support
# Copy & Paste? 
# MAIN_TEXT item should not contain (type); should be below or something. 
# MAIN_TEXT item should check if a string is binary or not: convert to '/000' style
# or convert newlines. 

version = "1.0"

import FrameWork
import EasyDialogs
import Dlg
import Res
import Qd
import List
import sys
from Types import *
from QuickDraw import *
import string
import time
import os

# The initial object to start browsing with. Can be anything, but 'sys' makes kindof sense.
start_object = sys

# Resource definitions
ID_MAIN = 503
NUM_LISTS = 4	# the number of lists used. could be changed, but the dlg item numbers should be consistent
MAIN_TITLE = 3	# this is only the first text item, the other three ID's should be 5, 7 and 9
MAIN_LIST = 4	# this is only the first list, the other three ID's should be 6, 8 and 10
MAIN_TEXT = 11
MAIN_LEFT = 1
MAIN_RIGHT = 2
MAIN_RESET = 12
MAIN_CLOSE = 13
MAIN_LINE = 14

def Initialize():
	# this bit ensures that this module will also work as an applet if the resources are
	# in the resource fork of the applet
	# stolen from Jack, so it should work(?!;-)
	try:
		# if this doesn't raise an error, we are an applet containing the necessary resources
		# so we don't have to bother opening the resource file
		dummy = Res.GetResource('DLOG', ID_MAIN)
	except Res.Error:
		savewd = os.getcwd()
		ourparentdir = os.path.split(openresfile.func_code.co_filename)[0]
		os.chdir(ourparentdir)		
		try:
			Res.OpenResFile("mactwit_browse.rsrc")
		except Res.Error, arg:
			EasyDialogs.Message("Cannot open mactwit_browse.rsrc: "+arg[1])
			sys.exit(1)
		os.chdir(savewd)

def main():
	Initialize()
	PythonBrowse()

# this is all there is to it to make an application. 
class PythonBrowse(FrameWork.Application):
	def __init__(self):
		FrameWork.Application.__init__(self)
		VarBrowser(self).open(start_object)
		self.mainloop()
	
	def do_about(self, id, item, window, event):
		EasyDialogs.Message(self.__class__.__name__ + " version " + version + "\rby Just van Rossum")
	
	def quit(self, *args):
		raise self

class MyList:
	def __init__(self, wid, rect, itemnum):
		# wid is the window (dialog) where our list is going to be in
		# rect is it's item rectangle (as in dialog item)
		# itemnum is the itemnumber in the dialog
		self.rect = rect
		rect2 = rect[0]+1, rect[1]+1, rect[2]-16, rect[3]-1		# Scroll bar space, that's 15 + 1, Jack!
		self.list = List.LNew(rect2, (0, 0, 1, 0), (0,0), 0, wid,
					0, 1, 0, 1)
		self.wid = wid
		self.active = 0
		self.itemnum = itemnum
	
	def setcontent(self, content, title = ""):
		# first, gather some stuff
		keylist = []
		valuelist = []
		thetype = type(content)
		if thetype == DictType:
			keylist = content.keys()
			keylist.sort()
			for key in keylist:
				valuelist.append(content[key])
		elif thetype == ListType:
			keylist = valuelist = content
		elif thetype == TupleType:
			
			keylist = valuelist = []
			for i in content:
				keylist.append(i)
		else:
			# XXX help me! is all this correct? is there more I should consider???
			# XXX is this a sensible way to do it in the first place????
			# XXX I'm not familiar enough with Python's guts to be sure. GUIDOOOOO!!!
			if hasattr(content, "__dict__"):
				keylist = keylist + content.__dict__.keys()
			if hasattr(content, "__methods__"):
				keylist = keylist + content.__methods__
			if hasattr(content, "__members__"):
				keylist = keylist + content.__members__
			if hasattr(content, "__class__"):
				keylist.append("__class__")
			if hasattr(content, "__bases__"):
				keylist.append("__bases__")
			if hasattr(content, "__name__"):
				title = content.__name__
				if "__name__" not in keylist:
					keylist.append("__name__")
			keylist.sort()
			for key in keylist:
				valuelist.append(getattr(content, key))
		if content <> None:
			title = title + "\r" + cleantype(content)
		# now make that list!
		tp, h, rect = self.wid.GetDialogItem(self.itemnum - 1)
		Dlg.SetDialogItemText(h, title[:255])
		self.list.LDelRow(0, 1)
		self.list.LSetDrawingMode(0)
		self.list.LAddRow(len(keylist), 0)
		for i in range(len(keylist)):
			self.list.LSetCell(str(keylist[i]), (0, i))
		self.list.LSetDrawingMode(1)
		self.list.LUpdate(self.wid.GetWindowPort().visRgn)
		self.content = content
		self.keylist = keylist
		self.valuelist = valuelist
		self.title = title
	
	# draw a frame around the list, List Manager doesn't do that
	def drawframe(self):
		Qd.SetPort(self.wid)
		Qd.FrameRect(self.rect)
		rect2 = Qd.InsetRect(self.rect, -3, -3)
		save = Qd.GetPenState()
		Qd.PenSize(2, 2)
		if self.active:
			Qd.PenPat(Qd.qd.black)
		else:
			Qd.PenPat(Qd.qd.white)
		# draw (or erase) an extra frame to indicate this is the acive list (or not)
		Qd.FrameRect(rect2)
		Qd.SetPenState(save)
		
		

class VarBrowser(FrameWork.DialogWindow):
	def open(self, start_object, title = ""):
		FrameWork.DialogWindow.open(self, ID_MAIN)
		if title <> "":
			windowtitle = self.wid.GetWTitle()
			self.wid.SetWTitle(windowtitle + " >> " + title)
		else:
			if hasattr(start_object, "__name__"):
				windowtitle = self.wid.GetWTitle()
				self.wid.SetWTitle(windowtitle + " >> " + str(getattr(start_object, "__name__")) )
				
		self.SetPort()
		Qd.TextFont(3)
		Qd.TextSize(9)
		self.lists = []
		self.listitems = []
		for i in range(NUM_LISTS):
			self.listitems.append(MAIN_LIST + 2 * i)	# dlg item numbers... have to be consistent
		for i in self.listitems:
			tp, h, rect = self.wid.GetDialogItem(i)
			list = MyList(self.wid, rect, i)
			self.lists.append(list)
		self.leftover = []
		self.rightover = []
		self.setup(start_object, title)
		
	def close(self):
		self.lists = []
		self.listitems = []
		self.do_postclose()
	
	def setup(self, start_object, title = ""):
		# here we set the starting point for our expedition
		self.start = start_object
		self.lists[0].setcontent(start_object, title)
		for list in self.lists[1:]:
			list.setcontent(None)
		
	def do_listhit(self, event, item):
		(what, message, when, where, modifiers) = event
		Qd.SetPort(self.wid)
		where = Qd.GlobalToLocal(where)
		for list in self.lists:
			list.active = 0
		list = self.lists[self.listitems.index(item)]
		list.active = 1
		for l in self.lists:
			l.drawframe()
		
		point = (0,0)
		ok, point = list.list.LGetSelect(1, point)
		if ok:
			oldsel = point[1]
		else:
			oldsel = -1
		# This should be: list.list.LClick(where, modifiers)
		# Since the selFlags field of the list is not accessible from Python I have to do it like this.
		# The effect is that you can't select more items by using shift or command.
		list.list.LClick(where, 0)
		
		index = self.listitems.index(item) + 1
		point = (0,0)
		ok, point = list.list.LGetSelect(1, point)
		if oldsel == point[1]:
			return	# selection didn't change, do nothing.
		if not ok:
			for i in range(index, len(self.listitems)):
				self.lists[i].setcontent(None)
			self.rightover = []
			return
			
		if point[1] >= len(list.keylist):
			return		# XXX is this still necessary? is ok really true?
		key = str(list.keylist[point[1]])
		value = list.valuelist[point[1]]
		
		self.settextitem("")
		thetype = type(value)
		if thetype == ListType or 				\
				thetype == TupleType or 		\
				thetype == DictType or 			\
				hasattr(value, "__dict__") or 		\
				hasattr(value, "__methods__") or	\
				hasattr(value, "__members__"):	# XXX or, or... again: did I miss something?
			if index >= len(self.listitems):
				# we've reached the right side of our dialog. move everything to the left
				# (by pushing the rightbutton...)
				self.do_rightbutton(1)
				index = index - 1
			newlist = self.lists[index]
			newlist.setcontent(value, key)
		else:
			index = index - 1
			self.settextitem( str(value) + "\r" + cleantype(value))
		for i in range(index + 1, len(self.listitems)):
			self.lists[i].setcontent(None)
		self.rightover = []
	
	# helper to set the big text item at the bottom of the dialog.
	def settextitem(self, text):
		tp, h, rect = self.wid.GetDialogItem(MAIN_TEXT)
		Dlg.SetDialogItemText(h, text[:255])
	
	def do_rawupdate(self, window, event):
		Qd.SetPort(self.wid)
		iType, iHandle, iRect = window.GetDialogItem(MAIN_LINE)
		Qd.FrameRect(iRect)
		for list in self.lists:
			Qd.FrameRect(list.rect)
			if list.active:
				# see MyList.drawframe
				rect2 = Qd.InsetRect(list.rect, -3, -3)
				save = Qd.GetPenState()
				Qd.PenSize(2, 2)
				Qd.FrameRect(rect2)
				Qd.SetPenState(save)
		for list in self.lists:
			list.list.LUpdate(self.wid.GetWindowPort().visRgn)
		
	def do_activate(self, activate, event):
		for list in self.lists:
			list.list.LActivate(activate)
		
	# scroll everything one 'unit' to the left
	# XXX I don't like the way this works. Too many 'manual' assignments
	def do_rightbutton(self, force = 0):
		if not force and self.rightover == []:
			return
		self.scroll(-1)
		point = (0, 0)
		ok, point = self.lists[0].list.LGetSelect(1, point)
		self.leftover.append(point, self.lists[0].content, self.lists[0].title, self.lists[0].active)
		for i in range(len(self.lists)-1):
			point = (0, 0)
			ok, point = self.lists[i+1].list.LGetSelect(1, point)
			self.lists[i].setcontent(self.lists[i+1].content, self.lists[i+1].title)
			self.lists[i].list.LSetSelect(ok, point)
			self.lists[i].list.LAutoScroll()
			self.lists[i].active = self.lists[i+1].active
			self.lists[i].drawframe()
		if len(self.rightover) > 0:
			point, content, title, active = self.rightover[-1]
			self.lists[-1].setcontent(content, title)
			self.lists[-1].list.LSetSelect(1, point)
			self.lists[-1].list.LAutoScroll()
			self.lists[-1].active = active
			self.lists[-1].drawframe()
			del self.rightover[-1]
		else:
			self.lists[-1].setcontent(None)
			self.lists[-1].active = 0
		for list in self.lists:
			list.drawframe()
	
	# scroll everything one 'unit' to the right
	def do_leftbutton(self):
		if self.leftover == []:
			return
		self.scroll(1)
		if self.lists[-1].content <> None:
			point = (0, 0)
			ok, point = self.lists[-1].list.LGetSelect(1, point)
			self.rightover.append(point, self.lists[-1].content, self.lists[-1].title, self.lists[-1].active )
		for i in range(len(self.lists)-1, 0, -1):
			point = (0, 0)
			ok, point = self.lists[i-1].list.LGetSelect(1, point)
			self.lists[i].setcontent(self.lists[i-1].content, self.lists[i-1].title)
			self.lists[i].list.LSetSelect(ok, point)
			self.lists[i].list.LAutoScroll()
			self.lists[i].active = self.lists[i-1].active
			self.lists[i].drawframe()
		if len(self.leftover) > 0:
			point, content, title, active = self.leftover[-1]
			self.lists[0].setcontent(content, title)
			self.lists[0].list.LSetSelect(1, point)
			self.lists[0].list.LAutoScroll()
			self.lists[0].active = active
			self.lists[0].drawframe()
			del self.leftover[-1]
		else:
			self.lists[0].setcontent(None)
			self.lists[0].active = 0
	
	# create some visual feedback when 'scrolling' the lists to the left or to the right
	def scroll(self, leftright):	# leftright should be 1 or -1
		# first, build a region containing all list rectangles
		myregion = Qd.NewRgn()
		mylastregion = Qd.NewRgn()
		for list in self.lists:
			AddRect2Rgn(list.rect, myregion)
			AddRect2Rgn(list.rect, mylastregion)
		# set the pen, but save it's state first
		self.SetPort()
		save = Qd.GetPenState()
		Qd.PenPat(Qd.qd.gray)
		Qd.PenMode(srcXor)
		# how far do we have to scroll?
		distance = self.lists[1].rect[0] - self.lists[0].rect[0]
		step = 30
		lasttime = time.clock()	# for delay
		# do it
		for i in range(0, distance, step):
			if i <> 0:
				Qd.FrameRgn(mylastregion)	# erase last region
				Qd.OffsetRgn(mylastregion, step * leftright, 0)
			# draw gray region
			Qd.FrameRgn(myregion)
			Qd.OffsetRgn(myregion, step * leftright, 0)
			while time.clock() - lasttime < 0.05:
				pass	# delay
			lasttime = time.clock()
		# clean up after your dog
		Qd.FrameRgn(mylastregion)
		Qd.SetPenState(save)
	
	def reset(self):
		for list in self.lists:
			point = (0,0)
			ok, point = list.list.LGetSelect(1, point)
			if ok:
				sel = list.keylist[point[1]]
			list.setcontent(list.content, list.title)
			if ok:
				list.list.LSetSelect(1, (0, list.keylist.index(sel)))
				list.list.LAutoScroll()
	
	def do_itemhit(self, item, event):
		if item in self.listitems:
			self.do_listhit(event, item)
		elif item == MAIN_LEFT:
			self.do_leftbutton()
		elif item == MAIN_RIGHT:
			self.do_rightbutton()
		elif item == MAIN_CLOSE:
			self.close()
		elif item == MAIN_RESET:
			self.reset()

# helper function that returns a short string containing the type of an arbitrary object
# eg: cleantype("wat is dit nu weer?") -> '(string)'
def cleantype(obj):
	# type() typically returns something like: <type 'string'>
	items = string.split(str(type(obj)), "'")
	if len(items) == 3:
		return '(' + items[1] + ')'
	else:
		# just in case, I don't know.
		return str(type(obj))
	
# helper for VarBrowser.scroll
def AddRect2Rgn(theRect, theRgn):
	rRgn = Qd.NewRgn()
	Qd.RectRgn(rRgn, theRect)
	Qd.UnionRgn(rRgn, theRgn, theRgn)


if __name__ == "__main__":
	main()
