Merge branch 'master' into tmbinc/FixTimingBugs
[enigma2.git] / lib / python / Screens / LocationBox.py
1 #
2 # Generic Screen to select a path/filename combination
3 #
4
5 # GUI (Screens)
6 from Screens.Screen import Screen
7 from Screens.MessageBox import MessageBox
8 from Screens.InputBox import InputBox
9 from Screens.HelpMenu import HelpableScreen
10 from Screens.ChoiceBox import ChoiceBox
11
12 # Generic
13 from Tools.BoundFunction import boundFunction
14 from Tools.Directories import *
15 from Components.config import config
16 import os
17
18 # Quickselect
19 from Tools.NumericalTextInput import NumericalTextInput
20
21 # GUI (Components)
22 from Components.ActionMap import NumberActionMap, HelpableActionMap
23 from Components.Label import Label
24 from Components.Pixmap import Pixmap
25 from Components.Button import Button
26 from Components.FileList import FileList
27 from Components.MenuList import MenuList
28
29 # Timer
30 from enigma import eTimer
31
32 class LocationBox(Screen, NumericalTextInput, HelpableScreen):
33         """Simple Class similar to MessageBox / ChoiceBox but used to choose a folder/pathname combination"""
34
35         skin = """<screen name="LocationBox" position="100,75" size="540,460" >
36                         <widget name="text" position="0,2" size="540,22" font="Regular;22" />
37                         <widget name="target" position="0,23" size="540,22" valign="center" font="Regular;22" />
38                         <widget name="filelist" position="0,55" zPosition="1" size="540,210" scrollbarMode="showOnDemand" selectionDisabled="1" />
39                         <widget name="textbook" position="0,272" size="540,22" font="Regular;22" />
40                         <widget name="booklist" position="5,302" zPosition="2" size="535,100" scrollbarMode="showOnDemand" />
41                         <widget name="red" position="0,415" zPosition="1" size="135,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
42                         <widget name="key_red" position="0,415" zPosition="2" size="135,40" halign="center" valign="center" font="Regular;22" transparent="1" shadowColor="black" shadowOffset="-1,-1" />   
43                         <widget name="green" position="135,415" zPosition="1" size="135,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
44                         <widget name="key_green" position="135,415" zPosition="2" size="135,40" halign="center" valign="center" font="Regular;22" transparent="1" shadowColor="black" shadowOffset="-1,-1" />
45                         <widget name="yellow" position="270,415" zPosition="1" size="135,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
46                         <widget name="key_yellow" position="270,415" zPosition="2" size="135,40" halign="center" valign="center" font="Regular;22" transparent="1" shadowColor="black" shadowOffset="-1,-1" />
47                         <widget name="blue" position="405,415" zPosition="1" size="135,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
48                         <widget name="key_blue" position="405,415" zPosition="2" size="135,40" halign="center" valign="center" font="Regular;22" transparent="1" shadowColor="black" shadowOffset="-1,-1" />            
49                 </screen>"""
50
51         def __init__(self, session, text = "", filename = "", currDir = None, bookmarks = None, userMode = False, windowTitle = _("Select Location"), minFree = None, autoAdd = False, editDir = False, inhibitDirs = [], inhibitMounts = []):
52                 # Init parents
53                 Screen.__init__(self, session)
54                 NumericalTextInput.__init__(self, handleTimeout = False)
55                 HelpableScreen.__init__(self)
56
57                 # Set useable chars
58                 self.setUseableChars(u'1234567890abcdefghijklmnopqrstuvwxyz')
59
60                 # Quickselect Timer
61                 self.qs_timer = eTimer()
62                 self.qs_timer.callback.append(self.timeout)
63                 self.qs_timer_type = 0
64
65                 # Initialize Quickselect
66                 self.curr_pos = -1
67                 self.quickselect = ""
68
69                 # Set Text
70                 self["text"] = Label(text)
71                 self["textbook"] = Label(_("Bookmarks"))
72
73                 # Save parameters locally
74                 self.text = text
75                 self.filename = filename
76                 self.minFree = minFree
77                 self.realBookmarks = bookmarks
78                 self.bookmarks = bookmarks and bookmarks.value[:] or []
79                 self.userMode = userMode
80                 self.autoAdd = autoAdd
81                 self.editDir = editDir
82                 self.inhibitDirs = inhibitDirs
83
84                 # Initialize FileList
85                 self["filelist"] = FileList(currDir, showDirectories = True, showFiles = False, inhibitMounts = inhibitMounts, inhibitDirs = inhibitDirs)
86
87                 # Initialize BookList
88                 self["booklist"] = MenuList(self.bookmarks)
89
90                 # Buttons
91                 self["key_green"] = Button(_("OK"))
92                 self["key_yellow"] = Button(_("Rename"))
93                 self["key_blue"] = Button(_("Remove Bookmark"))
94                 self["key_red"] = Button(_("Cancel"))
95
96                 # Background for Buttons
97                 self["green"] = Pixmap()
98                 self["yellow"] = Pixmap()
99                 self["blue"] = Pixmap()
100                 self["red"] = Pixmap()
101
102                 # Initialize Target
103                 self["target"] = Label()
104
105                 if self.userMode:
106                         self.usermodeOn()
107
108                 # Custom Action Handler
109                 class LocationBoxActionMap(HelpableActionMap):
110                         def __init__(self, parent, context, actions = { }, prio=0):
111                                 HelpableActionMap.__init__(self, parent, context, actions, prio)
112                                 self.box = parent
113
114                         def action(self, contexts, action):
115                                 # Reset Quickselect
116                                 self.box.timeout(force = True)
117
118                                 return HelpableActionMap.action(self, contexts, action)
119
120                 # Actions that will reset quickselect
121                 self["WizardActions"] = LocationBoxActionMap(self, "WizardActions",
122                         {
123                                 "left": self.left,
124                                 "right": self.right,
125                                 "up": self.up,
126                                 "down": self.down,
127                                 "ok": (self.ok, _("select")),
128                                 "back": (self.cancel, _("Cancel")),
129                         }, -2)
130
131                 self["ColorActions"] = LocationBoxActionMap(self, "ColorActions",
132                         {
133                                 "red": self.cancel,
134                                 "green": self.select,
135                                 "yellow": self.changeName,
136                                 "blue": self.addRemoveBookmark,
137                         }, -2)
138
139                 self["EPGSelectActions"] = LocationBoxActionMap(self, "EPGSelectActions",
140                         {
141                                 "prevBouquet": (self.switchToBookList, _("switch to bookmarks")),
142                                 "nextBouquet": (self.switchToFileList, _("switch to filelist")),
143                         }, -2)
144
145                 self["MenuActions"] = LocationBoxActionMap(self, "MenuActions",
146                         {
147                                 "menu": (self.showMenu, _("menu")),
148                         }, -2)
149
150                 # Actions used by quickselect
151                 self["NumberActions"] = NumberActionMap(["NumberActions"],
152                 {
153                         "1": self.keyNumberGlobal,
154                         "2": self.keyNumberGlobal,
155                         "3": self.keyNumberGlobal,
156                         "4": self.keyNumberGlobal,
157                         "5": self.keyNumberGlobal,
158                         "6": self.keyNumberGlobal,
159                         "7": self.keyNumberGlobal,
160                         "8": self.keyNumberGlobal,
161                         "9": self.keyNumberGlobal,
162                         "0": self.keyNumberGlobal
163                 })
164
165                 # Run some functions when shown
166                 self.onShown.extend([
167                         boundFunction(self.setTitle, windowTitle),
168                         self.updateTarget,
169                         self.showHideRename,
170                 ])
171
172                 self.onLayoutFinish.append(self.switchToFileListOnStart)
173  
174                 # Make sure we remove our callback
175                 self.onClose.append(self.disableTimer)
176
177         def switchToFileListOnStart(self):
178                 if self.realBookmarks and self.realBookmarks.value:
179                         self.currList = "booklist"
180                         currDir = self["filelist"].current_directory
181                         if currDir in self.bookmarks:
182                                 self["booklist"].moveToIndex(self.bookmarks.index(currDir))
183                 else:
184                         self.switchToFileList()
185
186         def disableTimer(self):
187                 self.qs_timer.callback.remove(self.timeout)
188
189         def showHideRename(self):
190                 # Don't allow renaming when filename is empty
191                 if self.filename == "":
192                         self["key_yellow"].hide()
193
194         def switchToFileList(self):
195                 if not self.userMode:
196                         self.currList = "filelist"
197                         self["filelist"].selectionEnabled(1)
198                         self["booklist"].selectionEnabled(0)
199                         self["key_blue"].text = _("Add Bookmark")
200                         self.updateTarget()
201
202         def switchToBookList(self):
203                 self.currList = "booklist"
204                 self["filelist"].selectionEnabled(0)
205                 self["booklist"].selectionEnabled(1)
206                 self["key_blue"].text = _("Remove Bookmark")
207                 self.updateTarget()
208
209         def addRemoveBookmark(self):
210                 if self.currList == "filelist":
211                         # add bookmark
212                         folder = self["filelist"].getSelection()[0]
213                         if folder is not None and not folder in self.bookmarks:
214                                 self.bookmarks.append(folder)
215                                 self.bookmarks.sort()
216                                 self["booklist"].setList(self.bookmarks)
217                 else:
218                         # remove bookmark
219                         if not self.userMode:
220                                 name = self["booklist"].getCurrent()
221                                 self.session.openWithCallback(
222                                         boundFunction(self.removeBookmark, name),
223                                         MessageBox,
224                                         _("Do you really want to remove your bookmark of %s?") % (name),
225                                 )
226
227         def removeBookmark(self, name, ret):
228                 if not ret:
229                         return
230                 if name in self.bookmarks:
231                         self.bookmarks.remove(name)
232                         self["booklist"].setList(self.bookmarks)
233
234         def createDir(self):
235                 if self["filelist"].current_directory != None:
236                         self.session.openWithCallback(
237                                 self.createDirCallback,
238                                 InputBox,
239                                 title = _("Please enter name of the new directory"),
240                                 text = ""
241                         )
242
243         def createDirCallback(self, res):
244                 if res is not None and len(res):
245                         path = os.path.join(self["filelist"].current_directory, res)
246                         if not pathExists(path):
247                                 if not createDir(path):
248                                         self.session.open(
249                                                 MessageBox,
250                                                 _("Creating directory %s failed.") % (path),
251                                                 type = MessageBox.TYPE_ERROR,
252                                                 timeout = 5
253                                         )
254                                 self["filelist"].refresh()
255                         else:
256                                 self.session.open(
257                                         MessageBox,
258                                         _("The path %s already exists.") % (path),
259                                         type = MessageBox.TYPE_ERROR,
260                                         timeout = 5
261                                 )
262
263         def removeDir(self):
264                 sel = self["filelist"].getSelection()
265                 if sel and pathExists(sel[0]):
266                         self.session.openWithCallback(
267                                 boundFunction(self.removeDirCallback, sel[0]),
268                                 MessageBox,
269                                 _("Do you really want to remove directory %s from the disk?") % (sel[0]),
270                                 type = MessageBox.TYPE_YESNO
271                         )
272                 else:
273                         self.session.open(
274                                 MessageBox,
275                                 _("Invalid directory selected: %s") % (sel[0]),
276                                 type = MessageBox.TYPE_ERROR,
277                                 timeout = 5
278                         )
279
280         def removeDirCallback(self, name, res):
281                 if res:
282                         if not removeDir(name):
283                                 self.session.open(
284                                         MessageBox,
285                                         _("Removing directory %s failed. (Maybe not empty.)") % (name),
286                                         type = MessageBox.TYPE_ERROR,
287                                         timeout = 5
288                                 )
289                         else:
290                                 self["filelist"].refresh()
291                                 self.removeBookmark(name, True)
292
293         def up(self):
294                 self[self.currList].up()
295                 self.updateTarget()
296
297         def down(self):
298                 self[self.currList].down()
299                 self.updateTarget()
300
301         def left(self):
302                 self[self.currList].pageUp()
303                 self.updateTarget()
304
305         def right(self):
306                 self[self.currList].pageDown()
307                 self.updateTarget()
308
309         def ok(self):
310                 if self.currList == "filelist":
311                         if self["filelist"].canDescent():
312                                 self["filelist"].descent()
313                                 self.updateTarget()
314                 else:
315                         self.select()
316
317         def cancel(self):
318                 self.close(None)
319
320         def getPreferredFolder(self):
321                 if self.currList == "filelist":
322                         # XXX: We might want to change this for parent folder...
323                         return self["filelist"].getSelection()[0]
324                 else:
325                         return self["booklist"].getCurrent()
326
327         def selectConfirmed(self, ret):
328                 if ret:
329                         ret = ''.join([self.getPreferredFolder(), self.filename])
330                         if self.realBookmarks:
331                                 if self.autoAdd and not ret in self.bookmarks:
332                                         self.bookmarks.append(self.getPreferredFolder())
333                                         self.bookmarks.sort()
334
335                                 if self.bookmarks != self.realBookmarks.value:
336                                         self.realBookmarks.value = self.bookmarks
337                                         self.realBookmarks.save()
338                         self.close(ret)
339
340         def select(self):
341                 currentFolder = self.getPreferredFolder()
342                 # Do nothing unless current Directory is valid
343                 if currentFolder is not None:
344                         # Check if we need to have a minimum of free Space available
345                         if self.minFree is not None:
346                                 # Try to read fs stats
347                                 try:
348                                         s = os.statvfs(currentFolder)
349                                         if (s.f_bavail * s.f_bsize) / 1000000 > self.minFree:
350                                                 # Automatically confirm if we have enough free disk Space available
351                                                 return self.selectConfirmed(True)
352                                 except OSError:
353                                         pass
354
355                                 # Ask User if he really wants to select this folder
356                                 self.session.openWithCallback(
357                                         self.selectConfirmed,
358                                         MessageBox,
359                                         _("There might not be enough Space on the selected Partition.\nDo you really want to continue?"),
360                                         type = MessageBox.TYPE_YESNO
361                                 )
362                         # No minimum free Space means we can safely close
363                         else:
364                                 self.selectConfirmed(True)
365
366         def changeName(self):
367                 if self.filename != "":
368                         # TODO: Add Information that changing extension is bad? disallow?
369                         self.session.openWithCallback(
370                                 self.nameChanged,
371                                 InputBox,
372                                 title = _("Please enter a new filename"),
373                                 text = self.filename
374                         )
375
376         def nameChanged(self, res):
377                 if res is not None:
378                         if len(res):
379                                 self.filename = res
380                                 self.updateTarget()
381                         else:
382                                 self.session.open(
383                                         MessageBox,
384                                         _("An empty filename is illegal."),
385                                         type = MessageBox.TYPE_ERROR,
386                                         timeout = 5
387                                 )
388
389         def updateTarget(self):
390                 # Write Combination of Folder & Filename when Folder is valid
391                 currFolder = self.getPreferredFolder()
392                 if currFolder is not None:
393                         self["target"].setText(''.join([currFolder, self.filename]))
394                 # Display a Warning otherwise
395                 else:
396                         self["target"].setText(_("Invalid Location"))
397
398         def showMenu(self):
399                 if not self.userMode and self.realBookmarks:
400                         menu = []
401                         if self.currList == "filelist":
402                                 menu.append((_("switch to bookmarks"), self.switchToBookList))
403                                 menu.append((_("add bookmark"), self.addRemoveBookmark))
404                                 if self.editDir:
405                                         menu.append((_("create directory"), self.createDir))
406                                         menu.append((_("remove directory"), self.removeDir))
407                         else:
408                                 menu.append((_("switch to filelist"), self.switchToFileList))
409                                 menu.append((_("remove bookmark"), self.addRemoveBookmark))
410
411                         self.session.openWithCallback(
412                                 self.menuCallback,
413                                 ChoiceBox,
414                                 title = "",
415                                 list = menu
416                         )
417
418         def menuCallback(self, choice):
419                 if choice:
420                         choice[1]()
421                         
422         def usermodeOn(self):
423                 self.switchToBookList()
424                 self["filelist"].hide()
425                 self["key_blue"].hide()
426
427         def keyNumberGlobal(self, number):
428                 # Cancel Timeout
429                 self.qs_timer.stop()
430
431                 # See if another key was pressed before
432                 if number != self.lastKey:
433                         # Reset lastKey again so NumericalTextInput triggers its keychange
434                         self.nextKey()
435
436                         # Try to select what was typed
437                         self.selectByStart()
438
439                         # Increment position
440                         self.curr_pos += 1
441
442                 # Get char and append to text
443                 char = self.getKey(number)
444                 self.quickselect = self.quickselect[:self.curr_pos] + unicode(char)
445
446                 # Start Timeout
447                 self.qs_timer_type = 0
448                 self.qs_timer.start(1000, 1)
449
450         def selectByStart(self):
451                 # Don't do anything on initial call
452                 if not len(self.quickselect):
453                         return
454
455                 # Don't select if no dir
456                 if self["filelist"].getCurrentDirectory():
457                         # TODO: implement proper method in Components.FileList
458                         files = self["filelist"].getFileList()
459
460                         # Initialize index
461                         idx = 0
462
463                         # We select by filename which is absolute
464                         lookfor = self["filelist"].getCurrentDirectory() + self.quickselect
465
466                         # Select file starting with generated text
467                         for file in files:
468                                 if file[0][0] and file[0][0].lower().startswith(lookfor):
469                                         self["filelist"].instance.moveSelectionTo(idx)
470                                         break
471                                 idx += 1
472
473         def timeout(self, force = False):
474                 # Timeout Key
475                 if not force and self.qs_timer_type == 0:
476                         # Try to select what was typed
477                         self.selectByStart()
478
479                         # Reset Key
480                         self.lastKey = -1
481
482                         # Change type
483                         self.qs_timer_type = 1
484
485                         # Start timeout again
486                         self.qs_timer.start(1000, 1)
487                 # Timeout Quickselect
488                 else:
489                         # Eventually stop Timer
490                         self.qs_timer.stop()
491
492                         # Invalidate
493                         self.lastKey = -1
494                         self.curr_pos = -1
495                         self.quickselect = ""
496
497         def __repr__(self):
498                 return str(type(self)) + "(" + self.text + ")"
499
500 class MovieLocationBox(LocationBox):
501         skinName = "LocationBox"
502
503         def __init__(self, session, text, dir, minFree = None):
504                 inhibitDirs = ["/bin", "/boot", "/dev", "/etc", "/lib", "/proc", "/sbin", "/sys", "/usr", "/var"]
505                 LocationBox.__init__(self, session, text = text, currDir = dir, bookmarks = config.movielist.videodirs, autoAdd = True, editDir = True, inhibitDirs = inhibitDirs, minFree = minFree)
506
507 class TimeshiftLocationBox(LocationBox):
508
509         skinName = "LocationBox" # XXX: though we could use a custom skin or inherit the hardcoded one we stick with the original :-)
510
511         def __init__(self, session):
512                 inhibitDirs = ["/bin", "/boot", "/dev", "/etc", "/lib", "/proc", "/sbin", "/sys", "/usr", "/var"]
513                 LocationBox.__init__(
514                                 self,
515                                 session,
516                                 text = _("Where to save temporary timeshift recordings?"),
517                                 currDir = config.usage.timeshift_path.value,
518                                 bookmarks = config.usage.allowed_timeshift_paths,
519                                 autoAdd = True,
520                                 editDir = True,
521                                 inhibitDirs = inhibitDirs,
522                                 minFree = 1024 # XXX: the same requirement is hardcoded in servicedvb.cpp
523                 )
524
525         def cancel(self):
526                 config.usage.timeshift_path.cancel()
527                 LocationBox.cancel(self)
528
529         def selectConfirmed(self, ret):
530                 if ret:
531                         config.usage.timeshift_path.value = self.getPreferredFolder()
532                         config.usage.timeshift_path.save()
533                         LocationBox.selectConfirmed(self, ret)
534