PlexKodiConnect/resources/lib/windows/kodigui.py

1019 lines
28 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from builtins import zip
from builtins import str
from builtins import range
from builtins import object
import xbmc
import xbmcgui
import time
import threading
import traceback
from .. import app
MONITOR = None
class BaseFunctions(object):
xmlFile = ''
path = ''
theme = ''
res = '720p'
width = 1280
height = 720
usesGenerate = False
lastWinID = None
def __init__(self):
self.isOpen = True
def onWindowFocus(self):
# Not automatically called. Can be used by an external window manager
pass
def onClosed(self):
pass
@classmethod
def open(cls, **kwargs):
window = cls(cls.xmlFile, cls.path, cls.theme, cls.res, **kwargs)
window.modal()
return window
@classmethod
def create(cls, show=True, **kwargs):
window = cls(cls.xmlFile, cls.path, cls.theme, cls.res, **kwargs)
if show:
window.show()
window.isOpen = True
return window
def modal(self):
self.isOpen = True
self.doModal()
self.onClosed()
self.isOpen = False
def activate(self):
if not self._winID:
self._winID = xbmcgui.getCurrentWindowId()
xbmc.executebuiltin('ReplaceWindow({0})'.format(self._winID))
def mouseXTrans(self, val):
return int((val / self.getWidth()) * self.width)
def mouseYTrans(self, val):
return int((val / self.getHeight()) * self.height)
def closing(self):
return self._closing
@classmethod
def generate(self):
return None
def setProperties(self, prop_list, val_list_or_val):
if isinstance(val_list_or_val, list) or isinstance(val_list_or_val, tuple):
val_list = val_list_or_val
else:
val_list = [val_list_or_val] * len(prop_list)
for prop, val in zip(prop_list, val_list):
self.setProperty(prop, val)
def propertyContext(self, prop, val='1'):
return WindowProperty(self, prop, val)
def setBoolProperty(self, key, boolean):
self.setProperty(key, boolean and '1' or '')
class BaseWindow(xbmcgui.WindowXML, BaseFunctions):
def __init__(self, *args, **kwargs):
BaseFunctions.__init__(self)
self._closing = False
self._winID = None
self.started = False
self.finishedInit = False
def onInit(self):
self._winID = xbmcgui.getCurrentWindowId()
BaseFunctions.lastWinID = self._winID
if self.started:
self.onReInit()
else:
self.started = True
self.onFirstInit()
self.finishedInit = True
def onFirstInit(self):
pass
def onReInit(self):
pass
def setProperty(self, key, value):
if self._closing:
return
if not self._winID:
self._winID = xbmcgui.getCurrentWindowId()
try:
xbmcgui.Window(self._winID).setProperty(key, value)
xbmcgui.WindowXML.setProperty(self, key, value)
except RuntimeError:
xbmc.log('kodigui.BaseWindow.setProperty: Missing window', xbmc.LOGDEBUG)
def doClose(self):
if not self.isOpen:
return
self._closing = True
self.isOpen = False
self.close()
def show(self):
self._closing = False
self.isOpen = True
xbmcgui.WindowXML.show(self)
def onClosed(self):
pass
class BaseDialog(xbmcgui.WindowXMLDialog, BaseFunctions):
def __init__(self, *args, **kwargs):
BaseFunctions.__init__(self)
self._closing = False
self._winID = ''
self.started = False
def onInit(self):
self._winID = xbmcgui.getCurrentWindowDialogId()
BaseFunctions.lastWinID = self._winID
if self.started:
self.onReInit()
else:
self.started = True
self.onFirstInit()
def onFirstInit(self):
pass
def onReInit(self):
pass
def setProperty(self, key, value):
if self._closing:
return
if not self._winID:
self._winID = xbmcgui.getCurrentWindowId()
try:
xbmcgui.Window(self._winID).setProperty(key, value)
xbmcgui.WindowXMLDialog.setProperty(self, key, value)
except RuntimeError:
xbmc.log('kodigui.BaseDialog.setProperty: Missing window', xbmc.LOGDEBUG)
def doClose(self):
self._closing = True
self.close()
def show(self):
self._closing = False
xbmcgui.WindowXMLDialog.show(self)
def onClosed(self):
pass
class ControlledBase(object):
def doModal(self):
self.show()
self.wait()
def wait(self):
while not self._closing and not MONITOR.waitForAbort(0.1):
pass
def close(self):
self._closing = True
class ControlledWindow(ControlledBase, BaseWindow):
def onAction(self, action):
try:
if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
self.doClose()
return
except Exception:
traceback.print_exc()
BaseWindow.onAction(self, action)
class ControlledDialog(ControlledBase, BaseDialog):
def onAction(self, action):
try:
if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
self.doClose()
return
except Exception:
traceback.print_exc()
BaseDialog.onAction(self, action)
DUMMY_LIST_ITEM = xbmcgui.ListItem()
class ManagedListItem(object):
def __init__(self, label='', label2='', iconImage='', thumbnailImage='',
path='', data_source=None, properties=None):
self._listItem = xbmcgui.ListItem(label, label2, iconImage, thumbnailImage, path)
self.dataSource = data_source
self.properties = {}
self.label = label
self.label2 = label2
self.iconImage = iconImage
self.thumbnailImage = thumbnailImage
self.path = path
self._ID = None
self._manager = None
self._valid = True
if properties:
for k, v in list(properties.items()):
self.setProperty(k, v)
def __bool__(self):
return self._valid
@property
def listItem(self):
if not self._listItem:
if not self._manager:
return None
try:
self._listItem = self._manager.getListItemFromManagedItem(self)
except RuntimeError:
return None
return self._listItem
def invalidate(self):
self._valid = False
self._listItem = DUMMY_LIST_ITEM
def _takeListItem(self, manager, lid):
self._manager = manager
self._ID = lid
self._listItem.setProperty('__ID__', lid)
li = self._listItem
self._listItem = None
self._manager._properties.update(self.properties)
return li
def _updateListItem(self):
self.listItem.setProperty('__ID__', self._ID)
self.listItem.setLabel(self.label)
self.listItem.setLabel2(self.label2)
self.listItem.setIconImage(self.iconImage)
self.listItem.setThumbnailImage(self.thumbnailImage)
self.listItem.setPath(self.path)
for k in list(self._manager._properties.keys()):
self.listItem.setProperty(k, self.properties.get(k) or '')
def clear(self):
self.label = ''
self.label2 = ''
self.iconImage = ''
self.thumbnailImage = ''
self.path = ''
for k in self.properties:
self.properties[k] = ''
self._updateListItem()
def pos(self):
if not self._manager:
return None
return self._manager.getManagedItemPosition(self)
def addContextMenuItems(self, items, replaceItems=False):
self.listItem.addContextMenuItems(items, replaceItems)
def addStreamInfo(self, stype, values):
self.listItem.addStreamInfo(stype, values)
def getLabel(self):
return self.label
def getLabel2(self):
return self.label2
def getProperty(self, key):
return self.properties.get(key, '')
def getdescription(self):
return self.listItem.getdescription()
def getduration(self):
return self.listItem.getduration()
def getfilename(self):
return self.listItem.getfilename()
def isSelected(self):
return self.listItem.isSelected()
def select(self, selected):
return self.listItem.select(selected)
def setArt(self, values):
return self.listItem.setArt(values)
def setIconImage(self, icon):
self.iconImage = icon
return self.listItem.setIconImage(icon)
def setInfo(self, itype, infoLabels):
return self.listItem.setInfo(itype, infoLabels)
def setLabel(self, label):
self.label = label
return self.listItem.setLabel(label)
def setLabel2(self, label):
self.label2 = label
return self.listItem.setLabel2(label)
def setMimeType(self, mimetype):
return self.listItem.setMimeType(mimetype)
def setPath(self, path):
self.path = path
return self.listItem.setPath(path)
def setProperty(self, key, value):
if self._manager:
self._manager._properties[key] = 1
self.properties[key] = value
self.listItem.setProperty(key, value)
return self
def setBoolProperty(self, key, boolean):
return self.setProperty(key, boolean and '1' or '')
def setSubtitles(self, subtitles):
return self.listItem.setSubtitles(subtitles) # List of strings - HELIX
def setThumbnailImage(self, thumb):
self.thumbnailImage = thumb
return self.listItem.setThumbnailImage(thumb)
def onDestroy(self):
pass
class ManagedControlList(object):
def __init__(self, window, control_id, max_view_index, data_source=None):
self.controlID = control_id
self.control = window.getControl(control_id)
self.items = []
self._sortKey = None
self._idCounter = 0
self._maxViewIndex = max_view_index
self._properties = {}
self.dataSource = data_source
def __getattr__(self, name):
return getattr(self.control, name)
def __getitem__(self, idx):
if isinstance(idx, slice):
return self.items[idx]
else:
return self.getListItem(idx)
def __iter__(self):
for i in self.items:
yield i
def __len__(self):
return self.size()
def _updateItems(self, bottom=None, top=None):
if bottom is None:
bottom = 0
top = self.size()
try:
for idx in range(bottom, top):
li = self.control.getListItem(idx)
mli = self.items[idx]
self._properties.update(mli.properties)
mli._manager = self
mli._listItem = li
mli._updateListItem()
except RuntimeError:
xbmc.log('kodigui.ManagedControlList._updateItems: Runtime error', xbmc.LOGNOTICE)
return False
return True
def _nextID(self):
self._idCounter += 1
return str(self._idCounter)
def reInit(self, window, control_id):
self.controlID = control_id
self.control = window.getControl(control_id)
self.control.addItems([i._takeListItem(self, self._nextID()) for i in self.items])
def setSort(self, sort):
self._sortKey = sort
def addItem(self, managed_item):
self.items.append(managed_item)
self.control.addItem(managed_item._takeListItem(self, self._nextID()))
def addItems(self, managed_items):
self.items += managed_items
self.control.addItems([i._takeListItem(self, self._nextID()) for i in managed_items])
def replaceItem(self, pos, mli):
self[pos].onDestroy()
self[pos].invalidate()
self.items[pos] = mli
li = self.control.getListItem(pos)
mli._manager = self
mli._listItem = li
mli._updateListItem()
def replaceItems(self, managed_items):
if not self.items:
self.addItems(managed_items)
return True
oldSize = self.size()
for i in self.items:
i.onDestroy()
i.invalidate()
self.items = managed_items
size = self.size()
if size != oldSize:
pos = self.getSelectedPosition()
if size > oldSize:
for i in range(0, size - oldSize):
self.control.addItem(xbmcgui.ListItem())
elif size < oldSize:
diff = oldSize - size
idx = oldSize - 1
while diff:
self.control.removeItem(idx)
idx -= 1
diff -= 1
if self.positionIsValid(pos):
self.selectItem(pos)
elif pos >= size:
self.selectItem(size - 1)
self._updateItems(0, self.size())
def getListItem(self, pos):
li = self.control.getListItem(pos)
mli = self.items[pos]
mli._listItem = li
return mli
def getListItemByDataSource(self, data_source):
for mli in self:
if data_source == mli.dataSource:
return mli
return None
def getSelectedItem(self):
pos = self.control.getSelectedPosition()
if not self.positionIsValid(pos):
pos = self.size() - 1
if pos < 0:
return None
return self.getListItem(pos)
def removeItem(self, index):
old = self.items.pop(index)
old.onDestroy()
old.invalidate()
self.control.removeItem(index)
top = self.control.size() - 1
if top < 0:
return
if top < index:
index = top
self.control.selectItem(index)
def removeManagedItem(self, mli):
self.removeItem(mli.pos())
def insertItem(self, index, managed_item):
pos = self.getSelectedPosition() + 1
if index >= self.size() or index < 0:
self.addItem(managed_item)
else:
self.items.insert(index, managed_item)
self.control.addItem(managed_item._takeListItem(self, self._nextID()))
self._updateItems(index, self.size())
if self.positionIsValid(pos):
self.selectItem(pos)
def moveItem(self, mli, dest_idx):
source_idx = mli.pos()
if source_idx < dest_idx:
rstart = source_idx
rend = dest_idx + 1
# dest_idx-=1
else:
rstart = dest_idx
rend = source_idx + 1
mli = self.items.pop(source_idx)
self.items.insert(dest_idx, mli)
self._updateItems(rstart, rend)
def swapItems(self, pos1, pos2):
if not self.positionIsValid(pos1) or not self.positionIsValid(pos2):
return False
item1 = self.items[pos1]
item2 = self.items[pos2]
li1 = item1._listItem
li2 = item2._listItem
item1._listItem = li2
item2._listItem = li1
item1._updateListItem()
item2._updateListItem()
self.items[pos1] = item2
self.items[pos2] = item1
return True
def shiftView(self, shift, hold_selected=False):
if not self._maxViewIndex:
return
selected = self.getSelectedItem()
selectedPos = selected.pos()
viewPos = self.getViewPosition()
if shift > 0:
pushPos = selectedPos + (self._maxViewIndex - viewPos) + shift
if pushPos >= self.size():
pushPos = self.size() - 1
self.selectItem(pushPos)
newViewPos = self._maxViewIndex
elif shift < 0:
pushPos = (selectedPos - viewPos) + shift
if pushPos < 0:
pushPos = 0
self.selectItem(pushPos)
newViewPos = 0
if hold_selected:
self.selectItem(selected.pos())
else:
diff = newViewPos - viewPos
fix = pushPos - diff
# print '{0} {1} {2}'.format(newViewPos, viewPos, fix)
if self.positionIsValid(fix):
self.selectItem(fix)
def reset(self):
self.dataSource = None
for i in self.items:
i.onDestroy()
i.invalidate()
self.items = []
self.control.reset()
def size(self):
return len(self.items)
def getViewPosition(self):
try:
return int(xbmc.getInfoLabel('Container({0}).Position'.format(self.controlID)))
except Exception:
return 0
def getViewRange(self):
viewPosition = self.getViewPosition()
selected = self.getSelectedPosition()
return list(range(max(selected - viewPosition, 0),
min(selected + (self._maxViewIndex - viewPosition) + 1,
self.size() - 1)))
def positionIsValid(self, pos):
return 0 <= pos < self.size()
def sort(self, sort=None, reverse=False):
sort = sort or self._sortKey
self.items.sort(key=sort, reverse=reverse)
self._updateItems(0, self.size())
def reverse(self):
self.items.reverse()
self._updateItems(0, self.size())
def getManagedItemPosition(self, mli):
return self.items.index(mli)
def getListItemFromManagedItem(self, mli):
pos = self.items.index(mli)
return self.control.getListItem(pos)
def topHasFocus(self):
return self.getSelectedPosition() == 0
def bottomHasFocus(self):
return self.getSelectedPosition() == self.size() - 1
def invalidate(self):
for item in self.items:
item._listItem = DUMMY_LIST_ITEM
def newControl(self, window=None, control_id=None):
self.controlID = control_id or self.controlID
self.control = window.getControl(self.controlID)
self.control.addItems([xbmcgui.ListItem() for i in range(self.size())])
self._updateItems()
class _MWBackground(ControlledWindow):
def __init__(self, *args, **kwargs):
self._multiWindow = kwargs.get('multi_window')
self.started = False
BaseWindow.__init__(self, *args, **kwargs)
def onInit(self):
if self.started:
return
self.started = True
self._multiWindow._open()
self.close()
class MultiWindow(object):
def __init__(self, windows=None, default_window=None, **kwargs):
self._windows = windows
self._next = default_window or self._windows[0]
self._properties = {}
self._current = None
self._allClosed = False
self.exitCommand = None
def __getattr__(self, name):
return getattr(self._current, name)
def setWindows(self, windows):
self._windows = windows
def setDefault(self, default):
self._next = default or self._windows[0]
def windowIndex(self, window):
if hasattr(window, 'MULTI_WINDOW_ID'):
for i, w in enumerate(self._windows):
if window.MULTI_WINDOW_ID == w.MULTI_WINDOW_ID:
return i
return 0
else:
return self._windows.index(window.__class__)
def nextWindow(self, window=None):
if window is False:
window = self._windows[self.windowIndex(self._current)]
if window:
if window.__class__ == self._current.__class__:
return None
else:
idx = self.windowIndex(self._current)
idx += 1
if idx >= len(self._windows):
idx = 0
window = self._windows[idx]
self._next = window
self._current.doClose()
return self._next
def _setupCurrent(self, cls):
self._current = cls(cls.xmlFile, cls.path, cls.theme, cls.res)
self._current.onFirstInit = self._onFirstInit
self._current.onReInit = self.onReInit
self._current.onClick = self.onClick
self._current.onFocus = self.onFocus
self._currentOnAction = self._current.onAction
self._current.onAction = self.onAction
@classmethod
def open(cls, **kwargs):
mw = cls(**kwargs)
b = _MWBackground(mw.bgXML, mw.path, mw.theme, mw.res, multi_window=mw)
b.modal()
del b
import gc
gc.collect(2)
return mw
def _open(self):
while not xbmc.Monitor().abortRequested() and not self._allClosed:
self._setupCurrent(self._next)
self._current.modal()
self._current.doClose()
del self._current
del self._next
del self._currentOnAction
def setProperty(self, key, value):
self._properties[key] = value
self._current.setProperty(key, value)
def _onFirstInit(self):
for k, v in list(self._properties.items()):
self._current.setProperty(k, v)
self.onFirstInit()
def doClose(self):
self._allClosed = True
self._current.doClose()
def onFirstInit(self):
pass
def onReInit(self):
pass
def onAction(self, action):
if action == xbmcgui.ACTION_PREVIOUS_MENU or action == xbmcgui.ACTION_NAV_BACK:
self.doClose()
self._currentOnAction(action)
def onClick(self, controlID):
pass
def onFocus(self, controlID):
pass
class SafeControlEdit(object):
CHARS_LOWER = 'abcdefghijklmnopqrstuvwxyz'
CHARS_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
CHARS_NUMBERS = '0123456789'
CURSOR = '[COLOR FFCC7B19]|[/COLOR]'
def __init__(self, control_id, label_id, window, key_callback=None, grab_focus=False):
self.controlID = control_id
self.labelID = label_id
self._win = window
self._keyCallback = key_callback
self.grabFocus = grab_focus
self._text = ''
self._compatibleMode = False
self.setup()
def setup(self):
self._labelControl = self._win.getControl(self.labelID)
self._winOnAction = self._win.onAction
self._win.onAction = self.onAction
self.updateLabel()
def setCompatibleMode(self, on):
self._compatibleMode = on
def onAction(self, action):
try:
controlID = self._win.getFocusId()
if controlID == self.controlID:
if self.processAction(action.getId()):
return
elif self.grabFocus:
if self.processOffControlAction(action.getButtonCode()):
self._win.setFocusId(self.controlID)
return
except Exception:
traceback.print_exc()
self._winOnAction(action)
def processAction(self, action_id):
if not self._compatibleMode:
self._text = self._win.getControl(self.controlID).getText()
if self._keyCallback:
self._keyCallback()
self. updateLabel()
return True
if 61793 <= action_id <= 61818: # Lowercase
self.processChar(self.CHARS_LOWER[action_id - 61793])
elif 61761 <= action_id <= 61786: # Uppercase
self.processChar(self.CHARS_UPPER[action_id - 61761])
elif 61744 <= action_id <= 61753:
self.processChar(self.CHARS_NUMBERS[action_id - 61744])
elif action_id == 61728: # Space
self.processChar(' ')
elif action_id == 61448:
self.delete()
else:
return False
if self._keyCallback:
self._keyCallback()
return True
def processOffControlAction(self, action_id):
if 61505 <= action_id <= 61530: # Lowercase
self.processChar(self.CHARS_LOWER[action_id - 61505])
elif 192577 <= action_id <= 192602: # Uppercase
self.processChar(self.CHARS_UPPER[action_id - 192577])
elif 61488 <= action_id <= 61497:
self.processChar(self.CHARS_NUMBERS[action_id - 61488])
elif 61552 <= action_id <= 61561:
self.processChar(self.CHARS_NUMBERS[action_id - 61552])
elif action_id == 61472: # Space
self.processChar(' ')
else:
return False
if self._keyCallback:
self._keyCallback()
return True
def _setText(self, text):
self._text = text
if not self._compatibleMode:
self._win.getControl(self.controlID).setText(text)
self.updateLabel()
def _getText(self):
if not self._compatibleMode and self._win.getFocusId() == self.controlID:
return self._win.getControl(self.controlID).getText()
else:
return self._text
def updateLabel(self):
self._labelControl.setLabel(self._getText() + self.CURSOR)
def processChar(self, char):
self._setText(self.getText() + char)
def setText(self, text):
self._setText(text)
def getText(self):
return self._getText()
def append(self, text):
self._setText(self.getText() + text)
def delete(self):
self._setText(self.getText()[:-1])
class PropertyTimer(object):
def __init__(self, window_id, timeout, property_, value='', init_value='1', addon_id=None, callback=None):
self._winID = window_id
self._timeout = timeout
self._property = property_
self._value = value
self._initValue = init_value
self._endTime = 0
self._thread = None
self._addonID = addon_id
self._closeWin = None
self._closed = False
self._callback = callback
def _onTimeout(self):
self._endTime = 0
xbmcgui.Window(self._winID).setProperty(self._property, self._value)
if self._addonID:
xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self._property), self._value)
if self._closeWin:
self._closeWin.doClose()
if self._callback:
self._callback()
def _wait(self):
while not xbmc.Monitor().abortRequested() and time.time() < self._endTime:
app.APP.monitor.waitForAbort(0.1)
if xbmc.Monitor().abortRequested():
return
if self._endTime == 0:
return
self._onTimeout()
def _stopped(self):
return not self._thread or not self._thread.isAlive()
def _reset(self):
self._endTime = time.time() + self._timeout
def _start(self):
self.init(self._initValue)
self._thread = threading.Thread(target=self._wait)
self._thread.start()
def stop(self, trigger=False):
self._endTime = trigger and 1 or 0
if not self._stopped():
self._thread.join()
def close(self):
self._closed = True
self.stop()
def init(self, val):
if val is False:
return
elif val is None:
val = self._initValue
xbmcgui.Window(self._winID).setProperty(self._property, val)
if self._addonID:
xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self._property), val)
def reset(self, close_win=None, init=None):
self.init(init)
if self._closed:
return
if not self._timeout:
return
self._closeWin = close_win
self._reset()
if self._stopped:
self._start()
class WindowProperty(object):
def __init__(self, win, prop, val='1', end=None):
self.win = win
self.prop = prop
self.val = val
self.end = end
self.old = self.win.getProperty(self.prop)
def __enter__(self):
self.win.setProperty(self.prop, self.val)
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
# re-raise any exception
return False
self.win.setProperty(self.prop, self.end or self.old)
class GlobalProperty(object):
def __init__(self, prop, val='1', end=None):
import xbmcaddon
self._addonID = xbmcaddon.Addon().getAddonInfo('id')
self.prop = prop
self.val = val
self.end = end
self.old = xbmc.getInfoLabel('Window(10000).Property({0}}.{1})'.format(self._addonID, prop))
def __enter__(self):
xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self.prop), self.val)
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
# re-raise any exception
return False
xbmcgui.Window(10000).setProperty('{0}.{1}'.format(self._addonID, self.prop), self.end or self.old)