"""
$Id: mythtvguide.py,v 1.11 2007/05/17 01:43:01 frooby Exp $
Copyright (C) 2005 Tom Warkentin <tom@ixionstudios.com>
"""
from mythtvgui import Dialog
from singleton import getInstance
from time import strftime,strptime
import datetime
import mythtv
import mythtvgui
import mythtvscheduledetails
import mythtvstruct
import mythtvutil
import traceback
import os
import xbmc
import xbmcgui
def debug( str ):
mythtvutil.debug( mythtvutil.DEBUG_GUI, str )
def showWindow(winDialog):
"""
Function to create the program guide window.
"""
debug( "> mythtvguide.showWindow()" )
mythtvgui.checkSettings()
win = Window()
win.loadskin( "programguide.xml" )
win.loadGuide()
winDialog.close()
win.doModal()
del win
debug( "< mythtvguide.showWindow()" )
class Window( mythtvgui.BaseWindow ):
# Mapping of Myth TV category names to color names (used as a file prefix)
# in the program guide.
categoryColors = {
"Adults only" : "red",
"Basketball" : "blue",
"Children" : "green",
"Children-music" : "green",
"Children-special" : "green",
"Fishing" : "blue",
"Hockey" : "blue",
"News" : "olive",
"Newsmagazine" : "olive",
"Romance" : "purple",
"Romance-comedy" : "purple",
"Science" : "cyan",
"Science fiction" : "orange",
"Sitcom" : "yellow",
"Soap" : "purple",
"Sports" : "blue",
"Sports event" : "blue",
"Sports non-event" : "blue",
"Talk" : "purple",
"Travel" : "cyan",
}
def __init__( self ):
mythtvgui.BaseWindow.__init__( self )
self.programs = []
self.startTime = None
self.endTime = None
self.startChan = None
self.endChan = None
self.channels = None
self.channelSpan = 2
self.hourSpan = 2.0
self.initialized = False
self.channelCtls = []
self.timeLabelCtls = []
self.progButtonCtls = []
self.topCtls = []
self.bottomCtls = []
self.leftCtls = []
self.rightCtls = []
self.prevFocus = None
self.prevButtonInfo = None
self.lock = 0
def addProgramControl( self,
program, info,
relX, relY, width, height ):
"""
Method to add a control for a program in the guide.
"""
debug( "> addProgramControl(program=[%s],info=[%s],relX=[%d],relY=[%d],"\
"width=[%d],height=[%d])"%(
program,info,relX,relY,width,height) )
info['program'] = program
if not program:
info['nodata'] = True
info['starttime'] = None
info['title'] = mythtvutil.getLocalizedString( 108 )
category = None
else:
info['nodata'] = False
info['starttime'] = program.starttime()
info['title'] = program.title()
if program.starttimeAsTime() < self.startTime:
info['title'] = "< " + info['title']
if program.endtimeAsTime() > self.endTime:
info['title'] += " >"
category = program.category()
info['start'] = relX
info['end'] = relX + width
# Create a button for navigation and hilighting. For some reason,
# button labels don't get truncated properly.
info['control'] = c = xbmcgui.ControlButton(
relX + self.guide_x, relY + self.guide_y, width, height, "",
mythtvutil.findMediaFile( self.getTexture(category, isFocus=True) ),
mythtvutil.findMediaFile( self.getTexture(category, isFocus=False) ),
alignment=(
mythtvgui.ALIGN_CENTER_Y |
mythtvgui.ALIGN_TRUNCATED ) )
self.addControl( c )
# Create a label to hold the name of the program. Label text seems to
# get truncated correctly...
info['label'] = c = xbmcgui.ControlLabel(
relX + self.guide_x, relY + self.guide_y, width, height,
info['title'],
alignment=mythtvgui.ALIGN_CENTER_Y | mythtvgui.ALIGN_TRUNCATED )
self.addControl( c )
self.progButtonCtls.append( info )
debug( "< addProgramControl()" )
return c
def checkPageUp( self, focusControl ):
"""
Method to check and to do a page up in the program guide.
Returns:
- False if no page change was done.
- True if a page change was done.
"""
paged = False
if focusControl in self.topCtls:
debug( "page up detected" )
paged = True
if self.startChan == 0:
# wrap around
pages = len( self.channels ) // self.channelSpan
index = len( self.channels ) - (
len( self.channels ) % self.channelSpan) - pages
self.setChannel( index )
else:
self.setChannel( self.startChan - (self.channelSpan - 1) )
self.loadGuide()
# check if we need to fix focus
if not self.prevFocus:
# find the control in the bottom row where previous button's
# start falls within start/end range of control
ctls = list( self.progButtonCtls )
ctls.reverse()
chanid = ctls[0]['chanid']
start = self.prevButtonInfo['start']
for c in ctls:
if chanid == c['chanid']:
if start >= c['start'] and \
start < c['end']:
self.prevFocus = c['control']
self.setFocus( self.prevFocus )
break
else:
break
return paged
def checkPageDown( self, focusControl ):
"""
Method to check and to do a page down in the program guide.
Returns:
- False if no page change was done.
- True if a page change was done.
"""
paged = False
if focusControl in self.bottomCtls:
debug( "page down detected" )
paged = True
if self.endChan == len( self.channels ) - 1:
# wrap around
self.setChannel( 0 )
else:
self.setChannel( self.startChan + (self.channelSpan - 1) )
self.loadGuide()
# check if we need to fix focus
if not self.prevFocus:
# find the control in the top row where previous button's start
# falls within start/end range of control
chanid = self.progButtonCtls[0]['chanid']
start = self.prevButtonInfo['start']
for c in self.progButtonCtls:
if chanid == c['chanid']:
if start >= c['start'] and \
start < c['end']:
self.prevFocus = c['control']
self.setFocus( self.prevFocus )
break
else:
break
return paged
def checkPageLeft( self, focusControl ):
"""
Method to check and to do a page left in the program guide.
Returns:
- False if no page change was done.
- True if a page change was done.
"""
paged = False
if focusControl in self.leftCtls:
debug( "page left detected" )
paged = True
delta = self.hourSpan - 0.5
startTime = self.startTime - datetime.timedelta( hours=delta )
self.setTime( startTime )
self.loadGuide()
# check if we need to fix focus
if not self.prevFocus:
chanid = self.prevButtonInfo['chanid']
found = False
prev = None
# find the right most program on the same channel
for c in self.progButtonCtls:
if not found and c['chanid'] == chanid:
found = True
elif found and c['chanid'] != chanid:
break
prev = c
self.prevFocus = prev['control']
self.setFocus( self.prevFocus )
self.prevButtonInfo = None
return paged
def checkPageRight( self, focusControl ):
"""
Method to check and to do a page right in the program guide.
Returns:
- False if no page change was done.
- True if a page change was done.
"""
paged = False
if focusControl in self.rightCtls:
debug( "page right detected" )
paged = True
delta = self.hourSpan - 0.5
startTime = self.startTime + datetime.timedelta( hours=delta )
self.setTime( startTime )
self.loadGuide()
# check if we need to fix focus
if not self.prevFocus:
chanid = self.prevButtonInfo['chanid']
found = False
prev = None
ctls = self.progButtonCtls
ctls.reverse()
# find the left most program on the same channel
for c in ctls:
if not found and c['chanid'] == chanid:
found = True
elif found and c['chanid'] != chanid:
break
prev = c
self.prevFocus = prev['control']
self.setFocus( self.prevFocus )
self.prevButtonInfo = None
return paged
def doNavigation( self ):
"""
Method to do navigation between controls and store lists of top,
bottom, left, and right controls to detect when page changes must
occur.
"""
count = 0
self.topCtls = []
self.bottomCtls = []
self.leftCtls = []
self.rightCtls = []
topChanId = None
prevChanId = None
prevCtl = None
#
# Loop through all buttons doing left to right, right to left, and
# top to bottom navigation. Also keep track of top, left, and right
# controls that are used to detect page up, left, and right.
#
for c in self.progButtonCtls:
#debug( "title=[%s]"%c['title'] )
if not topChanId:
topChanId = c['chanid']
if c['chanid'] == topChanId:
# first row of controls are top controls
self.topCtls.append( c['control'] )
#debug( "top ctl=[%s]"%c['control'] )
# do left to right and right to left navigation
if not prevChanId:
prevChanId = c['chanid']
elif prevChanId != c['chanid']:
# changed channel rows so previous control is a control on
# right edge
self.rightCtls.append( prevCtl )
prevCtl = None
prevChanId = c['chanid']
if prevCtl:
prevCtl.controlRight( c['control'] )
c['control'].controlLeft( prevCtl )
prevCtl = c['control']
if not prevCtl:
# control not set so this must be a control on left edge
self.leftCtls.append( c['control'] )
prevCtl = c['control']
# now find the appropriate control below current one
chanid = None
found = False
for c2 in self.progButtonCtls:
if not found and c2['control'] == c['control']:
found = True
elif found and not chanid and c2['chanid'] != c['chanid']:
chanid = c2['chanid']
if found and chanid and chanid == c2['chanid']:
if c['start'] >= c2['start'] and c['start'] < c2['end']:
c['control'].controlDown( c2['control'] )
#debug( "%s VVV %s"%(c['title'],c2['title']) )
count += 1
break
elif found and chanid and chanid != c2['chanid']:
break
debug( "down count=%d"%count )
count = 0
ctls = list(self.progButtonCtls)
ctls.reverse()
bottomChanId = None
#
# Loop through all buttons in reverse to do bottom to top navigation.
#
for c in ctls:
#debug( "title=[%s]"%c['title'] )
if not bottomChanId:
bottomChanId = c['chanid']
if c['chanid'] == bottomChanId:
# first row of controls are bottom controls
self.bottomCtls.append( c['control'] )
#debug( "bottom ctl=[%s]"%c['control'] )
# now find the control that is above the current one
chanid = None
found = False
for c2 in ctls:
if not found and c2['control'] == c['control']:
found = True
elif found and not chanid and c2['chanid'] != c['chanid']:
chanid = c2['chanid']
if found and chanid and chanid == c2['chanid']:
if c['start'] >= c2['start'] and c['start'] < c2['end']:
c['control'].controlUp( c2['control'] )
#debug( "%s ^^^ %s"%(c['title'],c2['title']) )
count += 1
break
elif found and chanid and chanid != c2['chanid']:
break
debug( "up count=%d"%count )
# if we have any controls, then the very last control on right edge
# was missed in first loop (right controls are detected by row changes
# but the last row quits the loop before detecting the control)
if len( ctls ) > 0:
# Note: This grabs last control from the reverse list of controls.
self.rightCtls.append( ctls[0]['control'] )
#debug( "right ctl=[%s]"%ctls[0]['control'] )
debug( "top count=%d"%len(self.topCtls) )
debug( "bottom count=%d"%len(self.bottomCtls) )
debug( "left count=%d"%len(self.leftCtls) )
debug( "right count=%d"%len(self.rightCtls) )
def getTexture( self, category, isFocus ):
"""
Method to figure out name of texture to use for the passed category.
"""
# determine color
if not category:
color = "shade"
else:
if self.categoryColors.has_key(category):
color = self.categoryColors[category]
else:
color = "shade"
# determine alpha value
if isFocus:
alpha = "50"
else:
alpha = "25"
# build texture file name
return "%s_%s.png"%(color,alpha)
def loadGuide( self ):
"""
Method to load and display the program guide information. If this is
the first time being called, it initializes the program guide
parameters.
"""
debug( '> mythtvguide.Window.loadGuide()' )
db = getInstance( mythtv.Database )
if self.prevFocus:
for c in self.progButtonCtls:
if c['control'] == self.prevFocus:
self.prevButtonInfo = c
self.prevFocus = None
break
if not self.initialized:
# load variables from skin
self.channel_x = int(self.getvalue( self.getoption( "channel_x" ) ))
self.channel_h = int(self.getvalue( self.getoption("channel_h" ) ))
self.channel_w = int(self.getvalue( self.getoption( "channel_w" ) ))
self.channel_dx = int(self.getvalue( self.getoption( "channel_dx" )) )
self.time_y = int(self.getvalue( self.getoption( "time_y" ) ))
self.time_h = int(self.getvalue( self.getoption( "time_h" ) ))
self.guide_x = int(self.getvalue( self.getoption( "guide_x" ) ))
self.guide_y = int(self.getvalue( self.getoption( "guide_y" ) ))
self.guide_dx = int(self.getvalue( self.getoption( "guide_dx" ) ))
self.guide_dy = int(self.getvalue( self.getoption( "guide_dy" ) ))
self.guide_w = int(self.getvalue( self.getoption( "guide_w" ) ))
self.guide_h = int(self.getvalue( self.getoption( "guide_h" ) ))
# calculate pixels per hour used repeatedly
self.widthPerHour = self.guide_w / self.hourSpan
# calculate channel span that fits into guide height
self.channelSpan = int(
self.guide_h /
(self.guide_dy+self.channel_h) )
debug( "channelSpan=[%d]"%self.channelSpan )
# allocate the remainder to vertical spacing between channels
remainder = self.guide_h // (self.guide_dy+self.channel_h)
self.guide_dy += (remainder / self.channelSpan)
# initialize channel range and time range
self.channels = db.getChannelList()
self.setChannel( 0 )
self.setTime(
datetime.datetime.now() - datetime.timedelta( minutes=30 ) )
self.initialized = True
self.programs = db.getProgramListings(
self.startTime, self.endTime,
self.channels[self.startChan].chanid(),
self.channels[self.endChan].chanid() )
debug( "found %d rows"%len(self.programs) )
self.render()
if not self.prevButtonInfo:
# set focus to the first control on the screen
if len( self.progButtonCtls ) > 0:
self.prevFocus = self.progButtonCtls[0]['control']
self.setFocus( self.prevFocus )
else:
raise Exception, "No program information available."
def onActionHook( self, action ):
"""
Method that is called whenever an event occurs in the GUI.
"""
debug( "> mythtvguide.Window.onActionHook( action=[%s] )"%action )
ctl = self.getFocus()
actionConsumed = False
if action == mythtvgui.ACTION_MOVE_DOWN:
actionConsumed = self.checkPageDown( self.prevFocus )
elif action == mythtvgui.ACTION_MOVE_UP:
actionConsumed = self.checkPageUp( self.prevFocus )
elif action == mythtvgui.ACTION_MOVE_LEFT:
actionConsumed = self.checkPageLeft( self.prevFocus )
elif action == mythtvgui.ACTION_MOVE_RIGHT:
actionConsumed = self.checkPageRight( self.prevFocus )
if not actionConsumed:
self.prevFocus = ctl
debug( "< mythtvguide.Window.onActionHook()" )
return actionConsumed
def onControlHook( self, control ):
"""
Method called when a control is selected/clicked.
"""
debug( "> mythtvguide.Window.onControlHook()" )
actionConsumed = 1
id = self.getcontrolid( control )
program = None
for c in self.progButtonCtls:
if c['control'] == control:
program = c['program']
break
if program:
debug( "converting program to schedule" )
schedule = mythtvstruct.ScheduleFromProgram( program )
debug( "launching schedule details window" )
rc = mythtvscheduledetails.showWindow( schedule )
debug( "< mythtvguide.Window.onControlHook()" )
return actionConsumed
def render( self ):
"""
Method to draw all the dynamic controls that represent the program
guide information.
"""
xbmcgui.lock()
try:
title = mythtvutil.getLocalizedString( 107 )
title += ": %s - %s"%(
self.startTime.strftime( "%x %X" ),
self.endTime.strftime( "%x %X" ) )
self.controls['title'].control.setLabel( title )
self.renderChannels()
self.renderTime()
self.renderPrograms()
self.doNavigation()
xbmcgui.unlock()
except:
xbmcgui.unlock()
raise
def renderChannels( self ):
"""
Method to draw the channel labels.
"""
try:
for c in self.channelCtls:
if c.has_key( 'icon' ):
self.removeControl( c['icon'] )
if c.has_key( 'label' ):
self.removeControl( c['label'] )
if c.has_key( 'shade' ):
self.removeControl( c['shade'] )
del c
self.channelCtls = []
cic = getInstance( mythtvgui.ChannelIconCache )
x = self.channel_x
y = self.guide_y
h = (self.guide_h - self.channelSpan * self.guide_dy) / self.channelSpan
iconW = h
labelW = self.channel_w - iconW - self.guide_dx
for i in range( self.startChan, self.endChan+1 ):
c = {}
# create shade image around channel label/icon
c['shade'] = xbmcgui.ControlImage(
x, y, self.channel_w, h,
mythtvutil.findMediaFile( "shade_50.png" ) )
self.addControl( c['shade'] )
# create label control
l = "%s %s"%(self.channels[i].channum(),self.channels[i].callsign())
c['label'] = xbmcgui.ControlLabel(
x+iconW+self.channel_dx, y, labelW, h,
l, self.getoption( "channel_font" ),
alignment=mythtvgui.ALIGN_CENTER_Y )
self.addControl( c['label'] )
shost = str(getInstance( mythtv.Settings ).getSetting( "mythtv_host" ))
file = mythtvgui.picBase + 'channels\\' + str(self.channels[i].channum()) + mythtvgui.picType
if not os.path.exists(file):
try:
file = cic.findFile( self.channels[i], shost )
except:
debug(" renderChannels: nothing assigned to file")
debug( "file=[%s]"%file )
if file:
# set channel icon
c['icon'] = xbmcgui.ControlImage( x, y, iconW, h, file )
self.addControl( c['icon'] )
self.channelCtls.append( c )
y += h + self.guide_dy
except:
debug("*** Failed to render channels ***")
raise
def renderPrograms( self ):
"""
Method to draw the program buttons. This manufactures buttons for
missing guide data.
"""
try:
# clear out old controls
for c in self.progButtonCtls:
self.removeControl( c['control'] )
del c['control']
self.removeControl( c['label'] )
del c['label']
self.progButtonCtls = []
self.widthPerHour = self.guide_w / self.hourSpan
chanH = (self.guide_h-self.channelSpan*self.guide_dy)/self.channelSpan
# Loop through each channel filling the program guide area with
# buttons.
for i in range( self.startChan, self.endChan+1 ):
noData = False
chanX = 0
chanY = (i-self.startChan) * (chanH + self.guide_dy)
chanid = self.channels[i].chanid()
# loop until we've filled the row for the channel
while chanX < self.guide_w:
ctlInfo = {}
ctlInfo['chanid'] = chanid
p = None
if not noData:
# find the next program for the channel - this assumes
# programs are sorted in ascending time order for the channel
for prog in self.programs:
if prog.chanid() == chanid:
p = prog
self.programs.remove( prog )
break
if not p:
# no program found - create a no data control for the rest of
# the row
noData = True
w = self.guide_w - chanX
self.addProgramControl(
program=None, info=ctlInfo,
relX=chanX, relY=chanY, width=w, height=chanH )
chanX += w
else:
# found a program but we don't know if it start at the
# current spot in the row for the channel
# clamp start time
start = p.starttimeAsTime()
if start < self.startTime:
start = self.startTime
# clamp end time
end = p.endtimeAsTime()
if end > self.endTime:
end = self.endTime
# calculate x coord and width of label
start = start - self.startTime
debug( "start=%s"%start )
progX = start.seconds / (60.0*60.0) * self.widthPerHour
end = end - self.startTime
debug( "end=%s"%end )
progEndX = end.seconds / (60.0*60.0) * self.widthPerHour
progW = progEndX - progX
# check if we need no data before control
if progX != chanX:
self.addProgramControl(
program=None, info=ctlInfo,
relX=chanX, relY=chanY,
width=(progX - chanX), height=chanH )
chanX = progX
ctlInfo = {}
ctlInfo['chanid'] = chanid
# add the control for the program
self.addProgramControl(
program=p, info=ctlInfo,
relX=progX, relY=chanY, width=progW, height=chanH )
chanX += progW
except:
debug("*** Failed to render programs ***")
raise
def renderTime( self ):
"""
Method to draw the time labels for the program guide.
"""
try:
doInit = False
if len( self.timeLabelCtls ) == 0:
doInit = True
numCols = int( self.hourSpan * 2 )
x = self.guide_x
y = self.time_y
h = self.time_h
w = (self.guide_w - numCols * self.guide_dx) / numCols
t = self.startTime
lastDay = t.day
i = 0
debug( "numCols=%d guide_w=%d"%(numCols, self.guide_w) )
while i < numCols:
if doInit:
debug( "x=%d y=%d w=%d h=%d"%(x,y,w,h) )
c = xbmcgui.ControlLabel(
x, y, w, h,
"", self.getoption("time_font") )
self.timeLabelCtls.append( c )
self.addControl( c )
l = t.strftime( "%H:%M" )
if t.day != lastDay:
l += "+1"
debug( "l=[%s]"%l )
self.timeLabelCtls[i].setLabel( l )
t += datetime.timedelta( minutes=30 )
i += 1
x = x + w + self.guide_dx
lastDay = t.day
except:
debug("*** Failed to render time ***")
raise
def setTime( self, startTime ):
"""
Method to change the starting time of the program guide. This is used
to change pages horizontally.
"""
self.startTime = startTime - datetime.timedelta(
seconds=startTime.second,
microseconds=startTime.microsecond )
min = self.startTime.minute
if min != 0:
if min > 30:
delta = 60 - min
else:
delta = 30 - min
self.startTime = self.startTime + \
datetime.timedelta( minutes=delta )
self.endTime = self.startTime + \
datetime.timedelta( hours=self.hourSpan )
debug( "startTime=[%s] endTime=[%s]"%(self.startTime,self.endTime) )
def setChannel( self, chanIndex ):
"""
Method to change the starting channel index of the program guide.
This is used to change pages vertically.
"""
self.startChan = chanIndex
if self.startChan < 0:
self.startChan = 0
self.endChan = self.startChan + self.channelSpan - 1
if self.endChan > len(self.channels)-1:
self.endChan = len(self.channels)-1
debug( "start channel=[%s]"%self.channels[self.startChan].channum() )
debug( "end channel=[%s]"%self.channels[self.endChan].channum() )
if __name__ == "__main__":
try:
mythtvutil.debugSetLevel( mythtvutil.DEBUG_SKIN | mythtvutil.DEBUG_GUI )
mythtvutil.initialize()
showWindow()
except Exception, ex:
traceback.print_exc()
Dialog().ok( mythtvutil.getLocalizedString( 27 ), str( ex ) )
|