""" Channel editor and adding dialogs, see channel_editor.ui, channel_add.ui, channel_process.ui """ ## MODULE IMPORTS ######################################################## import commands, os, sys # Can run stand-alone KBTV_MAINPATH = os.path.dirname(os.path.abspath(sys.argv[0])) if not KBTV_MAINPATH in sys.path: sys.path.append(KBTV_MAINPATH) import btcopyright, btdriver, btchannels from kdecore import KCmdLineArgs, KApplication, i18n from kdeui import KMessageBox from qt import QListViewItem, QDialog, QObject, SIGNAL, SLOT from channel_editor import ChannelEditor from channel_add import ChannelAdd from channel_process import ChannelAddProcess ## MODULE COPYRIGHT ###################################################### MODULE_AUTHOR = btcopyright.MY_NAME MODULE_AUTHOR_EMAIL = btcopyright.MY_EMAIL MODULE_COPYRIGHT = btcopyright.MY_COPYRIGHT MODULE_LICENSE = btcopyright.BSD_LICENSE MODULE_LICENSE_TEXT = btcopyright.BSD_LICENSE_TEXT ## MODULE CONSTANTS ###################################################### # Files, locations BTCHANLIST_CP = "CHANLIST.cp" BTCHANLIST_TXT = "CHANLIST" DOT_KBTV = "~/.kde/share/apps/kbtv" ERROR_READ = 0 ERROR_WRITE = 1 ## KBTVCHANNELEDITOR CLASS ############################################### class KbtvChannelEditor(ChannelEditor): """ Shows a dialog for managing and editing a list of TV channels. """ def __init__(self, *args): """ -> KbtvChannelEditor Creates a modal dialog showing a QListView containing the current channel list. The user can select one of them. There are buttons to move the selected channel up or down in the list, and for removing and adding channels. On adding, a KbtvChannelAdd dialog (modal to this one) is shown. If the OK button is hit, the list is written and the dialog closes. If the reset button is hit, the list goes back to the state when the dialog was created. On creation it attempts to read the BTCHANNELS_PC cPickle from DOT_KBTV first. If not present yet, it sets and writes the first channel (number 0) to the first external device (DEV0 usually). Finally the listview is populated and the BTChannelList object that was read or created can be manipulated using its methods. """ ChannelEditor.__init__(self, *args) self.dotdir = os.path.expanduser(DOT_KBTV) self.picklepath = os.path.join(self.dotdir, BTCHANLIST_CP) self.textpath = os.path.join(self.dotdir, BTCHANLIST_TXT) self.numjust = 2 # Right-justify chan number (column 0), ok up to 99 # Create empty chanlist self.chanlist = btchannels.BTChannelList() # Get/make us a chanlist if not os.path.exists(self.picklepath): if not os.path.exists(self.dotdir): try: os.mkdir(self.dotdir) except OSError: self.errorMsg(ERROR_WRITE) n = 0 if self.chanlist.tuner == n: # Are there even boards with the tuner at DEV0? n = 1 c = btchannels.BTComposite(n, "Composite") self.chanlist.append(c) self.writeList() else: self.readList() # Put it in the listview self.populateWidget(0) def writeList(self): """ -> void SLOT. Writes the chanlist. Shows an error box on failure. """ res = os.path.exists(self.dotdir) if not res: try: os.mkdir(self.dotdir) res = True except OSError: res = False res = (res and self.chanlist.writePickle(self.picklepath)) self.chanlist.writeText(self.textpath) if not res: self.errorMsg(ERROR_WRITE) def readList(self): """ -> void Reads the chanlist. Shows an error box on failure. """ res = self.chanlist.readPickle(self.picklepath) if not res: self.errorMsg(ERROR_READ) return res def errorMsg(self, e): """ -> void Shows an Error KMessageBox for e = ERROR_READ, ERROR_WRITE. """ if e == ERROR_READ: KMessageBox.error(self, i18n("Error reading from file") + "\n" + self.picklepath, i18n("Read error")) if e == ERROR_WRITE: KMessageBox.error(self, i18n("Error writing to file") + "\n" + self.picklepath, i18n("Write error")) def populateWidget(self, num): """ -> void Populates the ListView with channels. Num is the index to select. """ i = 0 len = self.chanlist.len() if self.chanlist.channels: # Index iteration ensures sorting while i < len: # self.listview is the ListView, inherited from ChannelEditor if self.chanlist.isBTComposite(self.chanlist.channels[i]): it = QListViewItem(self.listview, str(i).rjust(self.numjust, "0"), self.chanlist.channels[i].id, self.chanlist.channels[i].name) else: it = QListViewItem(self.listview, str(i).rjust(self.numjust, "0"), str(self.chanlist.channels[i].frequency), self.chanlist.channels[i].name) if i == num: self.listview.setSelected(it, True) i += 1 def resetList(self): """ -> void SLOT. Reads the (original) chanlist and replaces the current. Updates the ListView by clearing and repopulating it. """ self.readList() self.listview.clear() self.populateWidget(0) def removeChannel(self): """ -> void SLOT. Removes the channel currently selected from the chanlist and from the ListView. Updates the ListView by clearing and repopulating it. """ i = self.chanlist.currentChannelIndex() sel = self.listview.selectedItem() if sel: # Remove chan and associated listviewitem self.chanlist.deleteIndex(i) self.listview.takeItem(sel) self.listview.clear() self.populateWidget(i - 1) # works for i = 0 also (in python) def moveUp(self): """ -> void SLOT. Moves selected channel up in the list (cyclic). Up means up from the user's perspective. It means DOWN in the chanlist. """ sel = self.listview.selectedItem() if sel: self.chanlist.moveDown(self.chanlist.currentChannel()) above = sel.itemAbove() if above: tmp = sel.text(0) above.moveItem(sel) sel.setText(0, above.text(0)) above.setText(0, tmp) self.showChannelName() else: # Became channel max, must update whole listview self.listview.clear() self.populateWidget(self.chanlist.len() - 1) def moveDown(self): """ -> void SLOT. Moves selected channel down in the list (cyclic). Down means down from the user's perspective. It means UP in the chanlist. """ sel = self.listview.selectedItem() if sel: self.chanlist.moveUp(self.chanlist.currentChannel()) below = sel.itemBelow() if below: tmp = sel.text(0) sel.moveItem(below) sel.setText(0, below.text(0)) below.setText(0, tmp) self.showChannelName() else: # Became channel 0, need to repopulate self.listview.clear() self.populateWidget(0) def tuneTo(self): """ -> void SLOT. Tunes to selected channel (on selectionChanged SIGNAL). """ # QStrings vs Python strings, brrr self.chanlist.tuneToIndex(int(str(self.listview.selectedItem().text(0)))) def showChannelName(self): """ -> void SLOT. Shows the name of selected (on selectionChanged SIGNAL). """ self.editname.setText(self.listview.selectedItem().text(2)) def setChannelName(self): """ -> void SLOT. Sets name of selected in chanlist (on returnPressed SIGNAL). """ # QStrings vs Python strings, brrr txt = str(self.editname.text()) sel = self.listview.selectedItem() self.chanlist.currentChannel().setName(txt) sel.setText(2, txt) def addChannelsDialog(self): """ -> void SLOT. Runs the dialog(s) to add channels to the chanlist. Updates chanlist and its listview. """ adlg = KbtvChannelAdd(self.chanlist) # If OK clicked if adlg.exec_loop() == QDialog.Accepted: # One or more frequencies added if adlg.newfreqs: # Dialog for accepting or rejecting and manual fine tuning pdlg = KbtvChannelAddProcess(adlg.newfreqs, adlg.autotune) if pdlg.exec_loop() == QDialog.Accepted: sel = None for btchan in pdlg.newchans: i = self.chanlist.len() self.chanlist.append(btchan) # If a dupe it is not added to chanlist if i < self.chanlist.len(): sel = QListViewItem(self.listview, str(i).rjust(self.numjust, "0"), str(btchan.frequency), btchan.name) # Tune to last one if sel: self.listview.setSelected(sel, True) if adlg.newcomp: # Newcomp is a number, not the corresponding "EXTN" string i = self.chanlist.len() # Also tunes to it when the BTComposite is created btcomp = btchannels.BTComposite(adlg.newcomp, "") self.chanlist.append(btcomp) # If a dupe it is not added to chanlist (tuned anyway) if i < self.chanlist.len(): sel = QListViewItem(self.listview, str(i).rjust(self.numjust, "0"), btcomp.id, btcomp.name) self.listview.setSelected(sel, True) ## KBTVCHANNELADD CLASS ################################################## class KbtvChannelAdd(ChannelAdd): """ Shows a dialog to choose frequencies (by an entered list or by channelset) and/or an external input for addition to the channellist. """ def __init__(self, chanlist, *args): """ -> KbtvChannelAdd Takes chanlist as the current channel list. This is a dialog showing ways to input frequencies or a composite for becoming new members of the channel list. It doesn't do the addition itself, that's up to the calling code. The attribute newfreqs holds a list of frequencies, newcomp holds the numerical ID of a composite. """ ChannelAdd.__init__(self, *args) self.chanlist = chanlist self.newfreqs = [] self.newcomp = 0 self.autotune = False self.chansetitems = (btchannels.CHANSETS_PAL_NAMES + btchannels.CHANSETS_NTSC_NAMES + btchannels.CHANSETS_SECAM_NAMES) self.chansetitems.sort() self.populateChansets() self.populateComposites() if btdriver.use_driver == "saa": self.afc.setChecked(False) self.afc.setEnabled(False) def populateChansets(self): """ -> void Populates the chansets combobox with the available channelsets. """ self.chansets.insertItem(i18n("")) for item in self.chansetitems: self.chansets.insertItem(i18n(item[0])) def populateComposites(self): """ -> void Populates the composites combobox with (non-tuner) input devices. """ tdev = str(self.chanlist.tuner) comp = [] for item in self.chanlist.channels: if self.chanlist.isBTComposite(item): comp.append(item) self.composites.insertItem(i18n("")) for item in btchannels.COMPOSITE_NAMES: if not (item[-1:] == tdev): self.composites.insertItem(item) def getFreqList(self): """ -> list Returns the (total) list of frequencies as entered in the frequencies lineedit and/or from the chosen channelset. """ n, freqlist = "", [] s = str(self.frequencies.text()) for i in s: if not i.isdigit(): i = " " n += i for item in n.split(): f = int(item) if not f in freqlist: freqlist.append(f) c = self.chansets.currentItem() if c != 0: for f in self.chansetitems[c][1]: if not f in freqlist: freqlist.append(f) freqlist.sort() return freqlist def processChannels(self): """ -> void SLOT. When OK is clicked, the newfreqs and newcomp attributes are set according to the user's input. """ self.autotune = self.afc.isChecked() for item in self.getFreqList(): if not item in self.newfreqs: self.newfreqs.append(item) self.newfreqs.sort() c = self.composites.currentItem() if c != 0: self.newcomp = c if self.newcomp <= self.chanlist.tuner: self.newcomp -= 1 ## KBTVCHANNELADDPROCESS CLASS ########################################### class KbtvChannelAddProcess(ChannelAddProcess): """ Shows a processing dialog in which the user can step through a list of newly added or found frequencies, adjust their frequency (+/- 2 MHz), and can decide one by one which ones to keep or to discard. It is not possible to "step back". Processing can be stopped at any time. When processing is stopped or done the user can decide if the collection is to be added to her channel list or not. If the saa driver is used, there is no AFC. """ def __init__(self, newfreqs, autotune, *args): """ -> KbtvChannelAddProcess Takes a list of frequencies newfreqs and a boolean autotune (AFC), and shows a modal processing dialog. If AFC is on, it's only used to get and display the AFC'd frequency. The last-ditch manual fine tuning capability (spinbox) always uses non-AFC tuning. """ ChannelAddProcess.__init__(self, *args) self.newfreqs = newfreqs self.max = len(newfreqs) if btdriver.use_driver == "saa": self.autotune = False else: self.autotune = autotune self.newchans = [] self.current = 0 self.curchan = None self.progressbar.setTotalSteps(self.max) self.updateProgress() self.nextFreq() def stopProcessing(self): """ -> void SLOT. Executed when the Stop button is clicked. Disables the dialog widgets except the OK and Cancel button, which are disabled while processing. Also gets called when the last frequency has been processed. """ # Disable the frequency spinner, accept, reject, stop buttons self.stop.setEnabled(False) self.freqbox.setEnabled(False) self.acceptbut.setEnabled(False) self.rejectbut.setEnabled(False) # Enable the OK and Cancel button self.okbut.setEnabled(True) self.cancelbut.setEnabled(True) def nextFreq(self): """ -> void Put the next frequency in the spinbox and wait for the user to (modify) accept or reject it. Will tune to that frequency with or without AFC (according to the autotune attribute). """ if self.current < self.max: curfreq = self.newfreqs[self.current] # Also tunes to curfreq with or without AFC self.curchan = btchannels.BTChannel(curfreq, "", self.autotune) curfreq = self.curchan.frequency # AFC! self.freqbox.setMinValue(curfreq - 2) self.freqbox.setMaxValue(curfreq + 2) self.freqbox.setValue(curfreq) self.current += 1 else: self.stopProcessing() def acceptChannel(self): """ -> void SLOT. If the user clicks Accept the current channel is appended to the newchans attribute, holding BTChannel objects to be added to the channel list. Also calls the updateProgress() method to update the progress text and progressbar indicators. Calls nextFreq() after that. """ self.newchans.append(self.curchan) self.updateProgress() self.nextFreq() def rejectChannel(self): """ -> void SLOT. If the user clicks Reject. Only calls the updateProgress() method to update the progress text and progressbar indicators, and nextFreq(). """ self.updateProgress() self.nextFreq() def manualTune(self): """ -> void SLOT. If the user changes the frequency value in the spinbox. Creates a new BTChannel with the adjusted frequency, tunes to it (no AFC), and sets it to the current channel (curchan). """ curfreq = self.freqbox.value() # Also tunes to curfreq _without_ AFC self.curchan = btchannels.BTChannel(curfreq, "", False) def updateProgress(self): """ -> void Update the progress text and progressbar indicators. """ self.progresstext.setText(str(self.current) + "/" + str(self.max) + " - " + str(len(self.newchans))) self.progressbar.setProgress(self.current) ## END ################################################################### if __name__ == "__main__": appname = "kbtv_channels.py" description = "PyKDE dialog for channel list management" version = "1.2.4" KCmdLineArgs.init (sys.argv, appname, description, version) a = KApplication () QObject.connect(a,SIGNAL("lastWindowClosed()"),a,SLOT("quit()")) w = KbtvChannelEditor() a.setMainWidget(w) w.show() a.exec_loop()