#!/usr/bin/env python # -*- coding: utf-8 -*- 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, 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): """Magic method for bool(ManagedListItem).""" 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.setArt({'thumb': self.thumbnailImage, 'icon': self.iconImage}) 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 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 self.listItem.setArt({'thumb': self.thumbnailImage, 'icon': self.iconImage}) 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.is_alive() 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): self._addonID = 'plugin.video.plexkodiconnect' 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)