diff --git a/Changelog b/Changelog index 8ed846a4..3f19cfb1 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,13 @@ +2021-08-31 s-n-g + * Version 0.8.9.9 + * Search history navigation will work with normal keys + in addition to Control-key combinations (when a line + editor does not have the focus) + * When navigating to a new search term, in the RadioBrowser + Search Window, the two main check boxes will always get + the focus (makes it easier to navigate using normal keys) + * Docs Updated + 2021-08-22 s-n-g * Version 0.8.9.8 (0.9-beta5) * Fixing RadioBrowser save pop up window diff --git a/README.html b/README.html index 6e2f3d3f..d91d09e8 100644 --- a/README.html +++ b/README.html @@ -130,7 +130,7 @@
The best way to install PyRadio is via a distribution package, if one exists (e.g. Arch Linux and derivatives can install pyradio-git from AUR).
+The best way to install PyRadio is via a distribution package, if one exists (e.g. Arch Linux and derivatives can install any of these packages from the AUR).
In any other case, and since PyRadio is currently not available via pip, you will have to build it from source.
$ pyradio -h diff --git a/README.md b/README.md index adfbb89b..06775f26 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ and much more... ## Installation -The best way to install **PyRadio** is via a distribution package, if one exists (e.g. *Arch Linux* and derivatives can install [pyradio-git](https://aur.archlinux.org/packages/pyradio-git/) from AUR). +The best way to install **PyRadio** is via a distribution package, if one exists (e.g. *Arch Linux* and derivatives can install [any of these packages](https://aur.archlinux.org/packages/?K=pyradio) from the AUR). In any other case, and since **PyRadio** is currently not available via pip, you will have to [build it from source](build.md). @@ -364,7 +364,7 @@ This mode is designed to directly accept the "*?*" and "*\\*" characters (which The *Line editor* supports the insertion of [CJK Unified Ideographs](https://en.wikipedia.org/wiki/CJK_Unified_Ideographs), as described on [CJK Unified Ideographs (Unicode block)](https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)) also known as URO, abbreviation of Unified Repertoire and Ordering. These characters, although encoded as a single code-point (character), actually take up a 2-character space, when rendered on the terminal. -A depiction of the editor's behavior can be seen at this image: +A depiction of the editor's behavior can be seen at this image: ![CJK Characters on Pyradio](https://members.hellug.gr/sng/pyradio/pyradio-editor.jpg) @@ -702,9 +702,9 @@ Then, the mouse can be used as follows: **PyRadio** supports the following *Online radio directory services*: -- [RadioBrowser](https://www.radio-browser.info/) +- [RadioBrowser](https://www.radio-browser.info/) - This is a community driven effort (like wikipedia) with the aim of collecting as many internet radio and TV stations as possible. + This is a community driven effort (like wikipedia) with the aim of collecting as many internet radio and TV stations as possible. Read more at [PyRadio RadioBrowser Implementation](radio-browser.md) diff --git a/devel/build_install_pyradio b/devel/build_install_pyradio index e90fca91..d070c816 100755 --- a/devel/build_install_pyradio +++ b/devel/build_install_pyradio @@ -8,7 +8,7 @@ fi echo " Available options: 2 build using python v. 2.x - -u uninstall pyradio + -R remove (uninstall) pyradio --user user install (in ~/.local) If no option is used, will build using Python 3 @@ -178,7 +178,7 @@ do do_debian exit ;; - -u) + -R) uninstall exit ;; diff --git a/devel/pre-commit b/devel/pre-commit index fa5dd6c6..5e0c668c 100755 --- a/devel/pre-commit +++ b/devel/pre-commit @@ -104,7 +104,13 @@ do # convert images to links images_to_links "$out" if [ "$out" = "radio-browser.html" ];then - sed -i -e 's/N/^N/' -e 's/<.sup>P/^P/' -e 's|N, \^P|N, ^P|' "$out" + sed -i -e 's/N/^N/' \ + -e 's/<.sup>P/^P/' \ + -e 's|N, \^P|N, ^P|' \ + -e 's|X|^X|' \ + -e 's|<.strong>v|v|' \ + -e 's|<.strong><.sup>V<.strong>|^V|' \ + "$out" fi git add ${out} done diff --git a/pyradio/__init__.py b/pyradio/__init__.py index 86e12715..f4e2fcc5 100644 --- a/pyradio/__init__.py +++ b/pyradio/__init__.py @@ -1,6 +1,6 @@ " pyradio -- Console radio player. " -version_info = (0, 8, 9, 8) +version_info = (0, 8, 9, 9) # Application state: # New stable version: '' diff --git a/pyradio/browser.py b/pyradio/browser.py index dd149b25..de2b3a69 100644 --- a/pyradio/browser.py +++ b/pyradio/browser.py @@ -414,10 +414,12 @@ def stations(self, playlist_format=1): def save_config(self): ''' just an interface to config class save_config ''' - return self.browser_config.save_config(self._search_history, - self._default_search_history_index, - self._default_server, - self._default_max_number_of_results) + return self.browser_config.save_config( + self.AUTO_SAVE_CONFIG, + self._search_history, + self._default_search_history_index, + self._default_server, + self._default_max_number_of_results) def url(self, id_in_list): ''' Get a station's url using resolved_url @@ -1241,6 +1243,7 @@ def sort(self): self._sort.show() def read_config(self): + ''' RadioBrowser read config ''' self.browser_config.read_config() random_server = self._dns_info.give_me_a_server_url() # logger.error('DE random_server = {}'.format(random_server)) @@ -1249,6 +1252,7 @@ def read_config(self): logger.info('RadioBrowser: No server is reachable!') return False + self.AUTO_SAVE_CONFIG = self.browser_config.auto_save self._default_max_number_of_results = int(self.browser_config.limit) self._default_search_history_index = self._search_history_index = self.browser_config.default self._search_history = self.browser_config.terms @@ -1341,6 +1345,7 @@ def show_config(self, parent=None, init=False): self._config_win.show() class RadioBrowserConfig(object): + auto_save = False server = '' default = 1 limit = '100' @@ -1351,10 +1356,14 @@ def __init__(self, stations_dir): self.config_file = path.join(stations_dir, 'radio-browser-config') def read_config(self): + ''' RadioBrowserConfig read config ''' self.terms = [{ 'type': '', 'term': '100', 'post_data': {} }] + self.default = 1 + self.auto_save = False + self.limit = 100 lines = [] term_str = [] try: @@ -1379,12 +1388,13 @@ def read_config(self): sp[n] = sp[n].strip() # logger.error('DE sp = {}'.format(sp)) if sp[1]: - if sp[0] == 'DEFAULT_SERVER': + if sp[0] == 'AUTO_SAVE_CONFIG': + self.auto_save = True if sp[1].lower() == 'true' else False + elif sp[0] == 'DEFAULT_SERVER': self.server = sp[1] elif sp[0] == 'DEFAULT_LIMIT': - self.limit = sp[1] try: - x = int(self.limit) + self.limit = int(sp[1]) except: self.limit = '100' elif sp[0] == 'SEARCH_TERM': @@ -1401,8 +1411,12 @@ def read_config(self): try: self.terms.append(json.loads(term_str[n])) except: - if logger.isEnabledFor(logging.ERROR): - logger.error('RadioBrowser: error inserting search term {}'.format(n)) + try: + if logger.isEnabledFor(logging.ERROR): + logger.error('RadioBrowser: error inserting search term {}'.format(n)) + except: + if logger.isEnabledFor(logging.ERROR): + logger.error('RadioBrowser: error inserting serch item id {}'.format(n)) else: if logger.isEnabledFor(logging.DEBUG): logger.debug('RadioBrowser: no search terms found, reverting to defaults') @@ -1423,6 +1437,7 @@ def read_config(self): def save_config(self, + auto_save, search_history, search_default_history_index, default_server, @@ -1431,7 +1446,18 @@ def save_config(self, # RadioBrowser config file for PyRadio # ######################################## # +# Auto save config +# If True, the config will be automatically saved upon +# closing RadioBrowser. Otherwise, confirmation will be asked +# Possible values: True, False (default) +AUTO_SAVE_CONFIG = ''' + + txt += str(auto_save) + + txt += ''' + # Default server +# The server that RadioBrowser will use by default # Default: empty string (use random server) DEFAULT_SERVER = ''' @@ -1449,7 +1475,13 @@ def save_config(self, txt += ''' # List of "search terms" (queries) -# Default = "{'type': 'topvote', 'term': '100', 'post_data': {'reverse': 'true'}}" +# An asterisk specifies the default search term (the +# one activated when RadioBrowser opens up) +# Default = {'type': 'topvote', +# 'term': '100', +# 'post_data': {'reverse': 'true'} +# } +# ''' for n in range(1, len(search_history)): asterisk = '*' if n == search_default_history_index else '' @@ -1470,17 +1502,62 @@ def save_config(self, logger.info('Saved Online Browser config file') return True -class RadioBrowserConfighWindow(object): +class RadioBrowserConfigWindow(object): - _win = _widgets = _config = _history = None + TITLE = 'RadioBrowser Config' + _win = _widgets = _config = _history = _dns = None _default_history_id = _focus = 0 - _showed = False + _auto_save =_showed = False + invalid = False - def __init__(self, parent, config, current_history, default_history_id): + def __init__( + self, + parent, + config, + current_history=None, + default_history_id=-1, + limit=100, + ): self._win = self._parent = parent + self.maxY, self.maxX = self._parent.getmaxyx() self._config = config - self._history = deepcopy(history) - self._default_history_id = default_history_id + if history is not None: + ''' The window is not created ''' + self.invalid = True + + def read_config(self): + ''' RadioBrowserConfighWindow read config ''' + self._config.read_config() + self._dns = RadioBrowserDns() + random_server = self._dns_info.give_me_a_server_url() + # logger.error('DE random_server = {}'.format(random_server)) + if random_server is None: + if logger.isEnabledFor(logging.INFO): + logger.info('RadioBrowserConfighWindow: No server is reachable!') + return None + + self._auto_save = self._config.auto_save + self._default_limit = int(self._config.limit) + self._default_id = self._search_history_index = self._config.default + self._search_history = self._config.terms + self._default_server = self._config.server + if self._default_server: + self._server = self._default_server + if logger.isEnabledFor(logging.INFO): + logger.info('RadioBrowser: server is set by user: ' + self._server) + else: + self._server = random_server + if logger.isEnabledFor(logging.INFO): + logger.info('RadioBrowser: using random server: ' + self._server) + if logger.isEnabledFor(logging.INFO): + logger.info('RadioBrowser: result limit = {}'.format(self._default_limit)) + logger.info('RadioBrowser: default search term = {}'.format(self._default_id)) + logger.info('RadioBrowser: search history') + for i, n in enumerate(self._search_history): + logger.info(' {0}: {1}'.format(i, n)) + self._get_title() + return True + @property def focus(self): @@ -1499,7 +1576,84 @@ def focus(self, val): self.show() def show(self, parent, init=False): - self._win = self._parent = parent + if self.invalid: + return + pY, pX = self._parent.getmaxyx() + # logger.error('DE pY = {}, pX = {}'.format(pY, pX)) + self.Y, self.X = self._parent.getbegyx() + + if self.maxY != pY or self.maxX != pX: + pY, pX = self._parent.getmaxyx() + # logger.error('DE pY = {}, pX = {}'.format(pY, pX)) + self.maxY = pY + self.maxX = pX + self._win = self._parent + + if self.maxX < 80 or self.maxY < 22: + self._too_small = True + else: + self._too_small = False + + self._win.bkgdset(' ', curses.color_pair(5)) + self._win.erase() + self._win.box() + self._win.addstr(0, int((self.maxX - len(self.TITLE)) / 2), + self.TITLE, + curses.color_pair(4)) + + if self._too_small: + # TODO Print messge + msg = 'Window too small' + self._win.addstr( + int(self.maxY/2), int((self.maxX - len(msg))/2), + msg, curses.color_pair(5) + ) + self._win.refresh() + return + + if self._widgets is None: + self._widgets = [] + + self._widgets.append( + SimpleCursesBoolean( + Y=2, X=2, + window=self._win, + color=curses.color_pair(5), + color_focused=curses.color_pair(9), + color_not_focused=curses.color_pair(4), + color_disabled=curses.color_pair(5), + value=self._auto_save, + string='Auto save config: {0}' + ) + ) + self._widgets[-1] = len(self._widgets) - 1 + + self._widgets.append( + SimpleCursesCounter( + Y=5, X=2, + window=self._win, + color=curses.color_pair(5), + color_focused=curses.color_pair(9), + color_not_focused=curses.color_pair(4), + color_disabled=curses.color_pair(5), + minimum=0, maximum=1000, + step=1, big_step=10, + value=self._default_limit, + string='Maximum number of results: {0}' + ) + ) + self._widgets[-1] = len(self._widgets) - 1 + + for n in self._widgets(): + try: + self._widgets[n].show() + except: + self._widgets[n].show(self._win) + + self._win.addstr(3, 2, 'If True, no confirmation will be asked before saving', curses.color_pair(5)) + self._win.addstr(6, 2, 'A value of -1 will disable return items limiting', curses.color_pair(5)) + + self._win.refresh() self._showed = True @@ -1682,7 +1836,7 @@ def _search_term_to_widgets(self, a_search): self._widgets[i].checked = False self._widgets[i].enabled = False - self._focus = 1 + self._focus = 0 # logger.error('DE RADIO_BROWSER_DISPLAY_TERMS[a_search["type"]] = {}'.format(RADIO_BROWSER_DISPLAY_TERMS[a_search['type']])) else: @@ -1702,7 +1856,7 @@ def _search_term_to_widgets(self, a_search): # logger.error('DE Exact checked!!!') self._widgets[line_edit-1].checked = True self._widgets[line_edit].string = a_search['term'] - self._focus = line_edit + self._focus = 4 # logger.error('DE RADIO_BROWSER_SEARCH_BY_TERMS[a_search["type"]] = {}'.format(RADIO_BROWSER_SEARCH_BY_TERMS[a_search['type']])) elif a_search['type'] == 'search': @@ -1721,7 +1875,7 @@ def _search_term_to_widgets(self, a_search): self._widgets[s_id].checked = bool(n[1]) # logger.error('DE n[0] = {0}, n[1] = {1}, bool = {2}'.format(n[0], n[1], bool(n[1]))) s_id_list.append(s_id) - self._focus = min(s_id_list) + self._focus = 4 if a_search['post_data']: for n in a_search['post_data'].keys(): @@ -1759,7 +1913,7 @@ def _widgets_to_search_term(self): ret['term'] = str(self._widgets[-self.NUMBER_OF_WIDGETS_AFTER_SEARCH_SECTION].value) else: - ''' type is searchi (simple or advanced) ''' + ''' type is search (simple or advanced) ''' what_type = [] for i in range(5, len(self._widgets) - self.NUMBER_OF_WIDGETS_AFTER_SEARCH_SECTION): if type(self._widgets[i]).__name__ == 'SimpleCursesLineEdit': @@ -2014,9 +2168,9 @@ def show(self): X=self.yx[-2][1], window=self._win, color=curses.color_pair(5), - counter_color_focused=curses.color_pair(9), - counter_color_not_focused=curses.color_pair(4), - counter_color_disabled=curses.color_pair(5), + color_focused=curses.color_pair(9), + color_not_focused=curses.color_pair(4), + color_disabled=curses.color_pair(5), minimum=0, maximum=1000, step=1, big_step=10, value=self._default_limit, @@ -2230,6 +2384,62 @@ def _get_search_term_index(self, new_search_term): return found, index + def _ctrl_n(self): + ''' ^N - Next history item ''' + cur_history_id = self._selected_history_id + self._handle_new_or_existing_search_term() + if abs(self._selected_history_id - cur_history_id) > 1: + self._selected_history_id = cur_history_id + if len(self._history) > 1: + self._selected_history_id += 1 + if self._selected_history_id >= len(self._history): + self._selected_history_id = 0 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + + def _ctrl_p(self): + ''' ^P - Previous history item ''' + cur_history_id = self._selected_history_id + self._handle_new_or_existing_search_term() + if abs(self._selected_history_id - cur_history_id) > 1: + self._selected_history_id = cur_history_id + if len(self._history) > 1: + self._selected_history_id -= 1 + if self._selected_history_id <0: + self._selected_history_id = len(self._history) - 1 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + + def _ctrl_x(self): + ''' ^X - Delete history item ''' + self._handle_new_or_existing_search_term() + if len(self._history) > 2 and \ + self._selected_history_id > 0: + if self._default_history_id == self._selected_history_id: + self._default_history_id = 1 + self._history.pop(self._selected_history_id) + if self._selected_history_id == len(self._history): + self._selected_history_id -= 1 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + self._cnf.dirty = True + + def _ctrl_b(self): + ''' ^B - Set default item ''' + ret = self._handle_new_or_existing_search_term() + if self._selected_history_id > 0: + if ret == 1: + self._default_history_id = self._selected_history_id + self._print_history_legend() + self._win.refresh() + self._cnf.dirty = True + + def _ctrl_t(self): + ''' ^T - Go to template (item 0) ''' + self._selected_history_id = 0 + self._print_history_legend() + self._activate_search_term(self._history[self._selected_history_id]) + def keypress(self, char): ''' RadioBrowserSearchWindow keypress @@ -2273,29 +2483,12 @@ def keypress(self, char): elif char in (curses.ascii.SO, ): ''' ^N - Next history item ''' - cur_history_id = self._selected_history_id - self._handle_new_or_existing_search_term() - if abs(self._selected_history_id - cur_history_id) > 1: - self._selected_history_id = cur_history_id - if len(self._history) > 1: - self._selected_history_id += 1 - if self._selected_history_id >= len(self._history): - self._selected_history_id = 0 - self._print_history_legend() - self._activate_search_term(self._history[self._selected_history_id]) + logger.error('^N') + self._ctrl_n() elif char in (curses.ascii.DLE, ): ''' ^P - Previous history item ''' - cur_history_id = self._selected_history_id - self._handle_new_or_existing_search_term() - if abs(self._selected_history_id - cur_history_id) > 1: - self._selected_history_id = cur_history_id - if len(self._history) > 1: - self._selected_history_id -= 1 - if self._selected_history_id <0: - self._selected_history_id = len(self._history) - 1 - self._print_history_legend() - self._activate_search_term(self._history[self._selected_history_id]) + self._ctrl_p() elif char in (curses.ascii.SYN, ): ''' ^V - Save search history ''' @@ -2304,39 +2497,20 @@ def keypress(self, char): return 5 elif char in (curses.ascii.EM, ): - ''' ^T - Add history item ''' + ''' ^Y - Add history item ''' self._handle_new_or_existing_search_term() elif char in (curses.ascii.CAN, ): ''' ^X - Delete history item ''' - self._handle_new_or_existing_search_term() - if len(self._history) > 2 and \ - self._selected_history_id > 0: - if self._default_history_id == self._selected_history_id: - self._default_history_id = 1 - self._history.pop(self._selected_history_id) - - if self._selected_history_id == len(self._history): - self._selected_history_id -= 1 - self._print_history_legend() - self._activate_search_term(self._history[self._selected_history_id]) - self._cnf.dirty = True + self._ctrl_x() elif char in (curses.ascii.STX, ): ''' ^B - Set default item ''' - ret = self._handle_new_or_existing_search_term() - if self._selected_history_id > 0: - if ret == 1: - self._default_history_id = self._selected_history_id - self._print_history_legend() - self._win.refresh() - self._cnf.dirty = True + self._ctrl_b() elif char in (curses.ascii.DC4, ): - ''' ^Y - Add current item to history''' - self._selected_history_id = 0 - self._print_history_legend() - self._activate_search_term(self._history[self._selected_history_id]) + ''' ^T - Go to template (item 0) ''' + self._ctrl_t() else: if class_name == 'SimpleCursesWidgetColumns': @@ -2381,7 +2555,37 @@ def keypress(self, char): elif ret < 2: return 1 - if char in (ord('k'), curses.KEY_UP) and \ + if char in (ord('n'), ): + ''' ^N - Next history item ''' + self._ctrl_n() + + elif char in (ord('p'), ): + ''' ^P - Previous history item ''' + self._ctrl_p() + + elif char in (ord('v'), ): + ''' ^V - Save search history ''' + self._handle_new_or_existing_search_term() + ''' Save search history ''' + return 5 + + elif char in (ord('t'), ): + ''' ^T - Go to template (item 0) ''' + self._ctrl_t() + + elif char in (ord('x'), ): + ''' ^X - Delete history item ''' + self._ctrl_x() + + elif char in (ord('b'), ): + ''' ^B - Set default item ''' + self._ctrl_b() + + elif char in (ord('y'), ): + ''' ^Y - Add current item to history''' + self._handle_new_or_existing_search_term() + + elif char in (ord('k'), curses.KEY_UP) and \ class_name != 'SimpleCursesWidgetColumns': self._focus_up() elif char in (ord('j'), curses.KEY_DOWN) and \ diff --git a/pyradio/install.py b/pyradio/install.py index 2c905665..69e78329 100644 --- a/pyradio/install.py +++ b/pyradio/install.py @@ -27,6 +27,8 @@ except ModuleNotFoundError: HAVE_REQUESTS = False +VERSION = '0.8.9.9' + def is_pyradio_user_installed(): if platform.system().lower().startswith('darwin'): return False @@ -95,13 +97,14 @@ class PyRadioUpdate(object): 3 - official devel ''' - ZIP_URL = ('https://github.com/coderholic/pyradio/archive/master.zip', - 'https://github.com/s-n-g/pyradio/archive/master.zip', - 'https://github.com/s-n-g/pyradio/archive/devel.zip', - 'https://github.com/coderholic/pyradio/archive/devel.zip', - ) + ZIP_URL = ('https://github.com/coderholic/pyradio/archive/' + VERSION + '.zip', + 'https://github.com/s-n-g/pyradio/archive/master.zip', + 'https://github.com/s-n-g/pyradio/archive/devel.zip', + 'https://github.com/coderholic/pyradio/archive/devel.zip', + 'https://github.com/coderholic/pyradio/archive/master.zip', + ) - ZIP_DIR = ('pyradio-master', 'pyradio-master', 'pyradio-devel', 'pyradio-devel') + ZIP_DIR = ('pyradio-' + VERSION, 'pyradio-master', 'pyradio-devel', 'pyradio-devel', 'pyradio-master') install = False user = False @@ -165,7 +168,7 @@ def remove_pyradio(self, win_open_dir=False): sys.exit(1) def update_or_uninstall_on_windows(self, mode='update'): - params = ('', '--sng-master', '--sng-devel') + params = ('', '--sng-master', '--sng-devel', '--devel', '--master') isRunning() ''' Creates BAT file to update or unisntall PyRadio on Windows''' self._dir = os.path.join(os.path.expanduser('~'), 'tmp-pyradio') @@ -347,7 +350,8 @@ def _prompt_sudo(self): sys.exit(1) def _download_file(self, url, filename): - # print('url = "{0}", filename = "{1}"'.format(url, filename)) + print(' url: "{}"'.format(url)) + print(' filename: "{}"'.format(filename)) try: r = requests.get(url) except: @@ -434,9 +438,10 @@ def _do_it(self, mode='update'): --sng-devel download developer devel branch --force force installation (even if already installed) ''' + parser.add_argument('--master', action='store_true', help=SUPPRESS) + parser.add_argument('--devel', action='store_true', help=SUPPRESS) parser.add_argument('--sng-master', action='store_true', help=SUPPRESS) parser.add_argument('--sng-devel', action='store_true', help=SUPPRESS) - parser.add_argument('--devel', action='store_true', help=SUPPRESS) parser.add_argument('-f', '--force', action='store_true', help=SUPPRESS) args = parser.parse_args() @@ -445,11 +450,17 @@ def _do_it(self, mode='update'): ''' download official release ''' package = 0 if args.sng_master: + ''' sng master ''' package = 1 elif args.sng_devel: + '''' sng devel ''' package = 2 elif args.devel: + ''' official devel ''' package = 3 + elif args.master: + ''' official master ''' + package = 4 if args.uninstall: if platform.system().lower().startswith('win'): diff --git a/pyradio/radio.py b/pyradio/radio.py index a8f8ace8..509b3548 100644 --- a/pyradio/radio.py +++ b/pyradio/radio.py @@ -359,7 +359,7 @@ def __init__(self, pyradio_config, self.ws.RADIO_BROWSER_SEARCH_HELP_MODE: self._show_radio_browser_search_help, self.ws.BROWSER_PERFORMING_SEARCH_MODE: self._show_performing_search_message, self.ws.ASK_TO_SAVE_BROWSER_CONFIG: self._ask_to_save_browser_config, - self.ws.BROWSER_CONFIG_MODE: self._browser_init_config, + self.ws.RADIO_BROWSER_CONFIG_MODE: self._browser_init_config, } ''' list of help functions ''' @@ -736,6 +736,12 @@ def refreshBody(self, start=0): end = len(self._redisplay_list) if end == 0: end = 1 + if start == 0: + st = [i for i, x in enumerate(self._redisplay_list) if x[0] in self.ws.FULL_SCREEN_MODES] + if st: + start = st[-1] + if logger.isEnabledFor(logging.DEBUG): + logger.debug('refreshBody(): start = {}'.format(start)) for n in range(start, end): if n == 1: if self._theme_selector: @@ -1731,6 +1737,9 @@ def _show_radio_browser_search_help(self): Esc |Cancel operation. _ |Managing player volume does not work in search mode. + + |Search history navigation works with normal keys as well + |(|^N| is the same as |n| when not in a line editor). ''' self._show_help(txt, mode_to_set=self.ws.RADIO_BROWSER_SEARCH_HELP_MODE, @@ -1821,6 +1830,9 @@ def _show_main_help_page_4(self): I |Station |i|nfo (current selection). V ||V|ote for station. \\\\ q Escape |Close Browser (go back in history). + + |Search history navigation works with normal keys as well + |(|^N| is the same as |n| when not in a line editor). ''' self._show_help(txt, mode_to_set=self.ws.MAIN_HELP_MODE_PAGE_4, @@ -4679,7 +4691,7 @@ def keypress(self, char): self.ws.operation_mode not in (self.ws.EDIT_STATION_MODE, self.ws.ADD_STATION_MODE, self.ws.THEME_MODE, self.ws.RENAME_PLAYLIST_MODE, self.ws.CREATE_PLAYLIST_MODE, - self.ws.BROWSER_SEARCH_MODE, self.ws.BROWSER_CONFIG_MODE) and \ + self.ws.BROWSER_SEARCH_MODE, self.ws.RADIO_BROWSER_CONFIG_MODE, self.ws.RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE) and \ self.ws.operation_mode not in self.ws.PASSIVE_WINDOWS and \ not self.is_search_mode(self.ws.operation_mode) and \ self.ws.window_mode not in (self.ws.CONFIG_MODE, ): @@ -5130,7 +5142,7 @@ def keypress(self, char): self.refreshBody() return - elif self.ws.operation_mode == self.ws.BROWSER_CONFIG_MODE: + elif self.ws.operation_mode == self.ws.RADIO_BROWSER_CONFIG_MODE: ''' handle browser config ''' ret = self._cnf._online_browser.keypress(char) if ret == 0: @@ -6070,7 +6082,7 @@ def keypress(self, char): if self._cnf.browsing_station_service: self._print_not_implemented_yet() return - self.ws.operation_mode = self.ws.BROWSER_CONFIG_MODE + self.ws.operation_mode = self.ws.RADIO_BROWSER_CONFIG_MODE self._browser_init_config(init=True) else: if self._cnf.locked: diff --git a/pyradio/simple_curses_widgets.py b/pyradio/simple_curses_widgets.py index 03889920..34eb69d0 100644 --- a/pyradio/simple_curses_widgets.py +++ b/pyradio/simple_curses_widgets.py @@ -255,18 +255,18 @@ class SimpleCursesCounter(SimpleCursesWidget): with a number_length of 4, will display ' 11' color text color - counter_color_focused + color_focused counter color when enabled and focused - counter_color_not_focused + color_not_focused counter color when enabled but not focused - counter_color_disabled + color_disabled counter color when disabled ''' def __init__( self, Y, X, window, - color, counter_color_focused, - counter_color_not_focused, - counter_color_disabled, + color, color_focused, + color_not_focused, + color_disabled, minimum=0, maximum=100, step=1, big_step=5, value=1, number_length=3, string='{0}' @@ -289,9 +289,9 @@ def __init__( self._len = max_len self.string = string self._color = color - self._counter_color_focused = counter_color_focused - self._counter_color_not_focused = counter_color_not_focused - self._counter_color_disabled = counter_color_disabled + self._color_focused = color_focused + self._color_not_focused = color_not_focused + self._color_disabled = color_disabled def refresh(self): self.show(self._win) @@ -379,11 +379,11 @@ def show(self, window, opening=False): self._win = self._parent = window if self._enabled: if self._focused: - col = self._counter_color_focused + col = self._color_focused else: - col = self._counter_color_not_focused + col = self._color_not_focused else: - col = self._counter_color_disabled + col = self._color_disabled self._win.move(self._Y, self._X) if self._prefix: self._win.addstr(self._prefix, self._color) @@ -917,7 +917,7 @@ def keypress(self, char): return 1 -class SimpleMenuEntries(SimpleCursesWidget): +class SimpleCursesMenuEntries(SimpleCursesWidget): ''' A menu entries widget (a list of items vertically stacked) with selection and active item. @@ -1101,7 +1101,7 @@ def show(self): self._showed = True def keypress(self, char): - ''' SimpleMenuEntries keypress + ''' SimpleCursesMenuEntries keypress Returns ------- @@ -2822,7 +2822,102 @@ def return_history(self, direction, current_string): def reset_index(self): self._active_history_index = 0 -''' + +class SimpleCursesBoolean(SimpleCursesCounter): + ''' A class to provide a Boolean value + + Parameters + ========== + Y, X, window + Coordinates and parent window + value + the value, either Tru of False (default) + color + text color + color_focused + counter color when enabled and focused + color_not_focused + counter color when enabled but not focused + color_disabled + counter color when disabled + ''' + + def __init__( + self, Y, X, window, + color, color_focused, + color_not_focused, + color_disabled, + string='{0}', value=False + ): + self._Y = Y + self._X = X + self._win = self._parent = window + self._value = value + self.string = string + self._color = color + self._color_focused = color_focused + self._color_not_focused = color_not_focused + self._color_disabled = color_disabled + + def show(self, window=None): + if window: + self._win = self._parent = window + if self._enabled: + if self._focused: + col = self._color_focused + else: + col = self._color_not_focused + else: + col = self._color_disabled + self._win.move(self._Y, self._X) + if self._prefix: + self._win.addstr(self._prefix, self._color) + self._win.addstr(str(self._value), col) + if self._suffix: + self._win.addstr(self._suffix, self._color) + ''' overwrite last self._len characters ''' + self._win.addstr(' ' * 2, self._color) + self._showed = True + + def keypress(self, char): + ''' SimpleCursesBoolean keypress + + Returns + ------- + -1 - Cancel + 0 Value changed + 1 - Continue + 2 - Display help + ''' + if (not self._focused) or (not self._enabled): + return 1 + + if char in ( + curses.KEY_EXIT, ord('q'), 27, + ): + return -1 + + elif char == ord('?'): + return 2 + + elif char in (ord(' '), + ord('\n'), ord('\r'), + curses.KEY_ENTER, + ord('h'), + curses.KEY_LEFT, + ord('l'), + curses.KEY_RIGHT): + self.toggle() + self.show(self._win) + return 0 + + return 1 + + def toggle(self): + ''' toggles SimpleCursesBoolean value ''' + self._value = not self._value + + # # Testing part # @@ -2881,7 +2976,18 @@ def main(stdscr): on_left_callback_function=left, on_right_callback_function=right ) - a_widget.show() + # a_widget.show() + + a_widget = SimpleCursesBoolean( + Y=2, X=3, window=stdscr, + color=curses.color_pair(5), + color_focused=curses.color_pair(9), + color_not_focused=curses.color_pair(4), + color_disabled=curses.color_pair(5), + string='The valued is: {0}' + ) + a_widget.focused = True + a_widget.show(window=stdscr) while True: try: @@ -2925,4 +3031,4 @@ def __configureLogger(): if __name__ == "__main__": curses.wrapper(main) -''' + diff --git a/pyradio/window_stack.py b/pyradio/window_stack.py index 0a0a5db9..88dc8a4a 100644 --- a/pyradio/window_stack.py +++ b/pyradio/window_stack.py @@ -51,7 +51,8 @@ class Window_Stack_Constants(object): BROWSER_SEARCH_MODE = 71 BROWSER_OPEN_MODE = 72 BROWSER_PERFORMING_SEARCH_MODE = 73 - BROWSER_CONFIG_MODE = 74 + RADIO_BROWSER_CONFIG_MODE = 74 + RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE = 75 MAIN_HELP_MODE = 100 MAIN_HELP_MODE_PAGE_2 = 101 MAIN_HELP_MODE_PAGE_3 = 102 @@ -219,7 +220,8 @@ class Window_Stack_Constants(object): RADIO_BROWSER_SEARCH_HELP_MODE: 'RADIO_BROWSER_SEARCH_HELP_MODE', BROWSER_PERFORMING_SEARCH_MODE: 'BROWSER_PERFORMING_SEARCH_MODE', ASK_TO_SAVE_BROWSER_CONFIG: 'ASK_TO_SAVE_BROWSER_CONFIG', - BROWSER_CONFIG_MODE: 'BROWSER_CONFIG_MODE', + RADIO_BROWSER_CONFIG_MODE: 'RADIO_BROWSER_CONFIG_MODE', + RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE: 'RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE', } ''' When PASSIVE_WINDOWS target is one of them, @@ -233,11 +235,14 @@ class Window_Stack_Constants(object): ) FULL_SCREEN_MODES = ( + NORMAL_MODE, CONFIG_MODE, BROWSER_SEARCH_MODE, EDIT_STATION_MODE, ADD_STATION_MODE, RENAME_PLAYLIST_MODE, + RADIO_BROWSER_CONFIG_MODE, + RADIO_BROWSER_CONFIG_FROM_CONFIG_MODE, ) PASSIVE_WINDOWS = ( diff --git a/pyradio_rb.1 b/pyradio_rb.1 index cdbb3017..9d668b3e 100644 --- a/pyradio_rb.1 +++ b/pyradio_rb.1 @@ -215,6 +215,10 @@ Save the history. .RE .RS 5 +.IP \fBNote\fR +All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing "\fIx\fR" is the same as pressing "\fI^X\fR", "\fIv\fR" is the same as "\fI^V\fR" and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations. + +.P All history navigation actions (\fI^N\fR, \fI^P\fR, \fI^T\fR) will check if the data currently in the "form" fields can create a new \fBsearch term\fR and if so, will add it to the history. Tthe \fBSearch Window\fR actually works on a copy of the \fIsearch history\fR used by the service itself, so any changes made in it (adding and deleting items) are not passed to the service, until "\fIOK\fR" is pressed. Pressing "\fICancel\fR" will make all the changes go away. diff --git a/radio-browser.html b/radio-browser.html index 2c3c39eb..e1ca22db 100644 --- a/radio-browser.html +++ b/radio-browser.html @@ -207,6 +207,7 @@History Management
+Note: All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing “x” is the same as pressing “^X”, ”v” is the same as ”^V” and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations.
All history navigation actions (^N, ^P, ^T) will check if the data currently in the “form” fields can create a new search term and if so, will add it to the history.
The Search Window actually works on a copy of the search history used by the service itself, so any changes made in it (adding and deleting items or changing the default item) are not passed to the service, until “OK” is pressed. Pressing “Cancel” will make all the changes go away.
Even when “OK” is pressed, and the “Search Window” is closed, the “new” history is loaded into the service, but NOT saved to the configuration file.
diff --git a/radio-browser.md b/radio-browser.md index f4a2dea1..cfcdb36f 100644 --- a/radio-browser.md +++ b/radio-browser.md @@ -171,6 +171,8 @@ The keys to manage the history are all **Control** combinations: |**^B** |Make the current history item the **default** one for **RadioBrowser** and save the history.
This means that, next time you open **RadioBrowser** this history item ("**search term**") will be automatically loaded.| |**^V** |Save the history.| +**Note:** All keys can also be used without pressing the Control key, provided that a line editor does not have the focus. For example, pressing "**x**" is the same as pressing "**^X**", "**v**" is the same as "**^V**" and so on. This feature is provided for tiling window manager users who may have already assigned actions to any of these Contol-key combinations. + All history navigation actions (**^N**, **^P**, **^T**) will check if the data currently in the "form" fields can create a new **search term** and if so, will add it to the history. The **Search Window** actually works on a copy of the **search history** used by the service itself, so any changes made in it (adding and deleting items or changing the default item) are not passed to the service, until "**OK**" is pressed. Pressing "**Cancel**" will make all the changes go away.