diff --git a/deluge/ui/console/modes/alltorrents.py b/deluge/ui/console/modes/alltorrents.py index a45ee7875..049103666 100644 --- a/deluge/ui/console/modes/alltorrents.py +++ b/deluge/ui/console/modes/alltorrents.py @@ -330,10 +330,10 @@ class AllTorrents(BaseMode): td = TorrentDetail(self,tid,self.stdscr,self.encoding) component.get("ConsoleUI").set_mode(td) - def show_prefrences(self): + def show_preferences(self, core_config): component.stop(["AllTorrentsStateUpdater"]) self.stdscr.clear() - prefs = Preferences(self,self.stdscr,self.encoding) + prefs = Preferences(self,core_config,self.stdscr,self.encoding) component.get("ConsoleUI").set_mode(prefs) def _torrent_filter(self, idx, data): @@ -376,7 +376,6 @@ class AllTorrents(BaseMode): self.popup.add_line("Q_ueued",data=FILTER.QUEUED,foreground="yellow") def _do_add(self, result): - result["add_paused"] = (result["add_paused"] == "Yes") log.debug("Adding Torrent: %s (dl path: %s) (paused: %d)",result["file"],result["path"],result["add_paused"]) def suc_cb(msg): self.report_message("Torrent Added",msg) @@ -400,7 +399,7 @@ class AllTorrents(BaseMode): self.popup = InputPopup(self,"Add Torrent (Esc to cancel)",close_cb=self._do_add) self.popup.add_text_input("Enter path to torrent file:","file") self.popup.add_text_input("Enter save path:","path",dl) - self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],ap) + self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],[True,False],ap) def report_message(self,title,message): self.messages.append((title,message)) @@ -613,7 +612,7 @@ class AllTorrents(BaseMode): for l in HELP_LINES: self.popup.add_line(l) elif chr(c) == 'p': - self.show_prefrences() + client.core.get_config().addCallback(self.show_preferences) return self.refresh(effected_lines) diff --git a/deluge/ui/console/modes/input_popup.py b/deluge/ui/console/modes/input_popup.py index 459637d78..9c1dc4547 100644 --- a/deluge/ui/console/modes/input_popup.py +++ b/deluge/ui/console/modes/input_popup.py @@ -58,13 +58,121 @@ class InputField: return False def get_value(self): return None + def set_value(self, value): + pass + +class CheckedInput(InputField): + def __init__(self, parent, message, name, checked=False): + self.parent = parent + self.chkd_inact = "[X] %s"%message + self.unchkd_inact = "[ ] %s"%message + self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s"%message + self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s"%message + self.name = name + self.checked = checked + + def render(self, screen, row, width, active, col=1): + if self.checked and active: + self.parent.add_string(row,self.chkd_act,screen,col,False,True) + elif self.checked: + self.parent.add_string(row,self.chkd_inact,screen,col,False,True) + elif active: + self.parent.add_string(row,self.unchkd_act,screen,col,False,True) + else: + self.parent.add_string(row,self.unchkd_inact,screen,col,False,True) + return 1 + + def handle_read(self, c): + if c == 32: + self.checked = not self.checked + + def get_value(self): + return self.checked + + def set_value(self, c): + self.checked = c + +class IntSpinInput(InputField): + def __init__(self, parent, message, name, move_func, value, min_val, max_val): + self.parent = parent + self.message = message + self.name = name + self.value = int(value) + self.initvalue = self.value + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) + self.cursoff = len(self.message)+4 # + 4 for the " [ " in the rendered string + self.move_func = move_func + self.min_val = min_val + self.max_val = max_val + + def render(self, screen, row, width, active, col=1): + if not active and not self.valstr: + self.value = self.initvalue + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) + if not self.valstr: + self.parent.add_string(row,"%s [ ]"%self.message,screen,col,False,True) + elif active: + self.parent.add_string(row,"%s [ {!black,white,bold!}%d{!white,black!} ]"%(self.message,self.value),screen,col,False,True) + else: + self.parent.add_string(row,"%s [ %d ]"%(self.message,self.value),screen,col,False,True) + + if active: + self.move_func(row,self.cursor+self.cursoff) + + return 1 + + def handle_read(self, c): + if c == curses.KEY_PPAGE: + self.value+=1 + elif c == curses.KEY_NPAGE: + self.value-=1 + elif c == curses.KEY_LEFT: + self.cursor = max(0,self.cursor-1) + elif c == curses.KEY_RIGHT: + self.cursor = min(len(self.valstr),self.cursor+1) + elif c == curses.KEY_HOME: + self.cursor = 0 + elif c == curses.KEY_END: + self.cursor = len(self.value) + elif c == curses.KEY_BACKSPACE or c == 127: + if self.valstr and self.cursor > 0: + self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:] + self.cursor-=1 + if self.valstr: + self.value = int(self.valstr) + elif c == curses.KEY_DC: + if self.valstr and self.cursor < len(self.valstr): + self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:] + elif c > 47 and c < 58: + if c == 48 and self.cursor == 0: return + if self.cursor == len(self.valstr): + self.valstr += chr(c) + self.value = int(self.valstr) + else: + # Insert into string + self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:] + self.value = int(self.valstr) + # Move the cursor forward + self.cursor+=1 + + + def get_value(self): + return self.value + + def set_value(self, val): + self.value = int(val) + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) class SelectInput(InputField): - def __init__(self, parent, message, name, opts, selidx): + def __init__(self, parent, message, name, opts, vals, selidx): self.parent = parent self.message = message self.name = name self.opts = opts + self.vals = vals self.selidx = selidx def render(self, screen, row, width, selected, col=1): @@ -92,7 +200,14 @@ class SelectInput(InputField): self.selidx = min(len(self.opts)-1,self.selidx+1) def get_value(self): - return self.opts[self.selidx] + return self.vals[self.selidx] + + def set_value(self, nv): + for i,val in enumerate(self.vals): + if nv == val: + self.selidx = i + return + raise Exception("Invalid value for SelectInput") class TextInput(InputField): def __init__(self, parent, move_func, width, message, name, value, docmp): @@ -106,7 +221,7 @@ class TextInput(InputField): self.docmp = docmp self.tab_count = 0 - self.cursor = 0 + self.cursor = len(self.value) self.opts = None self.opt_off = 0 @@ -131,6 +246,10 @@ class TextInput(InputField): def get_value(self): return self.value + def set_value(self,val): + self.value = val + self.cursor = len(self.value) + # most of the cursor,input stuff here taken from ui/console/screen.py def handle_read(self,c): if c == 9 and self.docmp: @@ -264,8 +383,8 @@ class InputPopup(Popup): self.inputs.append(TextInput(self.parent, self.move, self.width, message, name, value, complete)) - def add_select_input(self, message, name, opts, default_index=0): - self.inputs.append(SelectInput(self.parent, message, name, opts, default_index)) + def add_select_input(self, message, name, opts, vals, default_index=0): + self.inputs.append(SelectInput(self.parent, message, name, opts, vals, default_index)) def _refresh_lines(self): self._cursor_row = -1 diff --git a/deluge/ui/console/modes/preference_panes.py b/deluge/ui/console/modes/preference_panes.py new file mode 100644 index 000000000..572adb5a4 --- /dev/null +++ b/deluge/ui/console/modes/preference_panes.py @@ -0,0 +1,212 @@ +# +# preference_panes.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may redistribute it and/or modify it under the terms of the +# GNU General Public License, as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) +# any later version. +# +# deluge is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +from deluge.ui.console.modes.input_popup import TextInput,SelectInput,CheckedInput,IntSpinInput + +try: + import curses +except ImportError: + pass + +import logging +log = logging.getLogger(__name__) + + +class Header: + def __init__(self, parent, header, space_above, space_below): + self.parent = parent + self.header = "{!white,black,bold!}%s"%header + self.space_above = space_above + self.space_below = space_below + + def render(self, screen, row, width, active, offset): + rows = 1 + if self.space_above: + row += 1 + rows += 1 + self.parent.add_string(row,self.header,screen,offset-1,False,True) + if self.space_below: rows += 1 + return rows + + +class BasePane: + def __init__(self, offset, parent, width): + self.offset = offset+1 + self.parent = parent + self.width = width + self.inputs = [] + self.active_input = -1 + + def move(self,r,c): + self._cursor_row = r + self._cursor_col = c + + def add_config_values(self,conf_dict): + for ipt in self.inputs: + if not isinstance(ipt,Header): + conf_dict[ipt.name] = ipt.get_value() + + def update_values(self, conf_dict): + for ipt in self.inputs: + if not isinstance(ipt,Header): + try: + ipt.set_value(conf_dict[ipt.name]) + except KeyError: # just ignore if it's not in dict + pass + + def render(self, mode, screen, width, active): + self._cursor_row = -1 + if self.active_input < 0: + for i,ipt in enumerate(self.inputs): + if not isinstance(ipt,Header): + self.active_input = i + break + crow = 1 + for i,ipt in enumerate(self.inputs): + act = active and i==self.active_input + crow += ipt.render(screen,crow,width, act, self.offset) + + if active and self._cursor_row >= 0: + curses.curs_set(2) + screen.move(self._cursor_row,self._cursor_col+self.offset-1) + else: + curses.curs_set(0) + + # just handles setting the active input + def handle_read(self,c): + if not self.inputs: # no inputs added yet + return + + if c == curses.KEY_UP: + nc = max(0,self.active_input-1) + while isinstance(self.inputs[nc], Header): + nc-=1 + if nc <= 0: break + if not isinstance(self.inputs[nc], Header): + self.active_input = nc + elif c == curses.KEY_DOWN: + ilen = len(self.inputs) + nc = min(self.active_input+1,ilen-1) + while isinstance(self.inputs[nc], Header): + nc+=1 + if nc >= ilen: break + if not isinstance(self.inputs[nc], Header): + self.active_input = nc + else: + self.inputs[self.active_input].handle_read(c) + + + def add_header(self, header, space_above=False, space_below=False): + self.inputs.append(Header(self.parent, header, space_above, space_below)) + + def add_text_input(self, name, msg, dflt_val): + self.inputs.append(TextInput(self.parent,self.move,self.width,msg,name,dflt_val,False)) + + def add_select_input(self, name, msg, opts, vals, selidx): + self.inputs.append(SelectInput(self.parent,msg,name,opts,vals,selidx)) + + def add_checked_input(self, name, message, checked): + self.inputs.append(CheckedInput(self.parent,message,name,checked)) + + def add_int_spin_input(self, name, message, value, min_val, max_val): + self.inputs.append(IntSpinInput(self.parent,message,name,self.move,value,min_val,max_val)) + + +class DownloadsPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + + self.add_header("Folders") + self.add_text_input("download_location","Download To:",parent.core_config["download_location"]) + self.add_header("Allocation") + + if parent.core_config["compact_allocation"]: + alloc_idx = 1 + else: + alloc_idx = 0 + self.add_select_input("compact_allocation","Allocation:",["Use Full Allocation","Use Compact Allocation"],[False,True],alloc_idx) + self.add_header("Options",True) + self.add_checked_input("prioritize_first_last_pieces","Prioritize first and last pieces of torrent",parent.core_config["prioritize_first_last_pieces"]) + self.add_checked_input("add_paused","Add torrents in paused state",parent.core_config["add_paused"]) + + +class NetworkPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + +class BandwidthPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Global Bandwidth Usage") + self.add_int_spin_input("max_connections_global","Maximum Connections:",parent.core_config["max_connections_global"],0,1000) + self.add_int_spin_input("max_upload_slots_global","Maximum Upload Slots:",parent.core_config["max_upload_slots_global"],0,1000) + #self.add_int_spin_input("max_download_speed","Maximum Download Speed (KiB/s):",-1,0,1000) + +class InterfacePane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + # does classic mode make sense in console? + #self.add_header("Classic Mode") + #self.add_checked_input("classic_mode","Enable",False) + + # add title bar control here + +class OtherPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("GeoIP Database") + self.add_text_input("geoip_db_location","Location:",parent.core_config["geoip_db_location"]) + +class DaemonPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Port") + self.add_int_spin_input("daemon_port","Daemon Port:",parent.core_config["daemon_port"],0,1000) + self.add_header("Connections",True) + self.add_checked_input("allow_remote","Allow remote connections",parent.core_config["allow_remote"]) + self.add_header("Other",True) + self.add_checked_input("new_release_check","Periodically check the website for new releases",parent.core_config["new_release_check"]) + +class QueuePane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + +class ProxyPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + +class CachePane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) diff --git a/deluge/ui/console/modes/preferences.py b/deluge/ui/console/modes/preferences.py index 32b3c4c4f..b59b49932 100644 --- a/deluge/ui/console/modes/preferences.py +++ b/deluge/ui/console/modes/preferences.py @@ -35,9 +35,12 @@ # import deluge.component as component +from deluge.ui.client import client from basemode import BaseMode from input_popup import SelectInput +from preference_panes import DownloadsPane,NetworkPane,BandwidthPane,InterfacePane +from preference_panes import OtherPane,DaemonPane,QueuePane,ProxyPane,CachePane from collections import deque @@ -56,7 +59,7 @@ class ZONE: ACTIONS = 2 class Preferences(BaseMode): - def __init__(self, parent_mode, stdscr, encoding=None): + def __init__(self, parent_mode, core_config, stdscr, encoding=None): self.parent_mode = parent_mode self.categories = [_("Downloads"), _("Network"), _("Bandwidth"), _("Interface"), _("Other"), _("Daemon"), _("Queue"), _("Proxy"), @@ -66,13 +69,30 @@ class Preferences(BaseMode): self.messages = deque() self.action_input = None + self.core_config = core_config + self.active_zone = ZONE.CATEGORIES # how wide is the left 'pane' with categories self.div_off = 15 - BaseMode.__init__(self, stdscr, encoding) - self.action_input = SelectInput(self,None,None,["Cancel","Apply","OK"],0) + BaseMode.__init__(self, stdscr, encoding, False) + + # create the panes + self.prefs_width = self.cols-self.div_off-1 + self.panes = [ + DownloadsPane(self.div_off+2, self, self.prefs_width), + NetworkPane(self.div_off+2, self, self.prefs_width), + BandwidthPane(self.div_off+2, self, self.prefs_width), + InterfacePane(self.div_off+2, self, self.prefs_width), + OtherPane(self.div_off+2, self, self.prefs_width), + DaemonPane(self.div_off+2, self, self.prefs_width), + QueuePane(self.div_off+2, self, self.prefs_width), + ProxyPane(self.div_off+2, self, self.prefs_width), + CachePane(self.div_off+2, self, self.prefs_width) + ] + + self.action_input = SelectInput(self,None,None,["Cancel","Apply","OK"],[0,1,2],0) self.refresh() def __draw_catetories(self): @@ -85,11 +105,13 @@ class Preferences(BaseMode): self.add_string(i+1,"- %s"%category) self.stdscr.vline(1,self.div_off,'|',self.rows-2) + def __draw_preferences(self): + self.panes[self.cur_cat].render(self,self.stdscr, self.prefs_width, self.active_zone == ZONE.PREFRENCES) + def __draw_actions(self): - if self.action_input: - selected = self.active_zone == ZONE.ACTIONS - self.stdscr.hline(self.rows-3,self.div_off+1,"_",self.cols) - self.action_input.render(self.stdscr,self.rows-2,self.cols,selected,self.cols-22) + selected = self.active_zone == ZONE.ACTIONS + self.stdscr.hline(self.rows-3,self.div_off+1,"_",self.cols) + self.action_input.render(self.stdscr,self.rows-2,self.cols,selected,self.cols-22) def refresh(self): if self.popup == None and self.messages: @@ -103,6 +125,9 @@ class Preferences(BaseMode): self.__draw_catetories() self.__draw_actions() + + # do this last since it moves the cursor + self.__draw_preferences() self.stdscr.noutrefresh() @@ -119,7 +144,32 @@ class Preferences(BaseMode): self.cur_cat = min(len(self.categories)-1,self.cur_cat+1) def __prefs_read(self, c): - pass + self.panes[self.cur_cat].handle_read(c) + + def __apply_prefs(self): + new_core_config = {} + for pane in self.panes: + pane.add_config_values(new_core_config) + # Apply Core Prefs + if client.connected(): + # Only do this if we're connected to a daemon + config_to_set = {} + for key in new_core_config.keys(): + # The values do not match so this needs to be updated + if self.core_config[key] != new_core_config[key]: + config_to_set[key] = new_core_config[key] + + if config_to_set: + # Set each changed config value in the core + client.core.set_config(config_to_set) + client.force_call(True) + # Update the configuration + self.core_config.update(config_to_set) + + def __update_preferences(self,core_config): + self.core_config = core_config + for pane in self.panes: + pane.update_values(core_config) def __actions_read(self, c): self.action_input.handle_read(c) @@ -128,10 +178,10 @@ class Preferences(BaseMode): if self.action_input.selidx == 0: # cancel self.back_to_parent() elif self.action_input.selidx == 1: # apply - # TODO: Actually apply - pass + self.__apply_prefs() + client.core.get_config().addCallback(self.__update_preferences) elif self.action_input.selidx == 2: # OK - # TODO: Actually apply + self.__apply_prefs() self.back_to_parent() @@ -160,7 +210,7 @@ class Preferences(BaseMode): reactor.stop() return - elif c == 9: + if c == 9: self.active_zone += 1 if self.active_zone > ZONE.ACTIONS: self.active_zone = ZONE.CATEGORIES