def _calculate_popup_geometry(textwidget: tkinter.Text) -> str: bbox = textwidget.bbox('insert') assert bbox is not None # cursor must be visible (cursor_x, cursor_y, cursor_width, cursor_height) = bbox # make coordinates relative to screen cursor_x += textwidget.winfo_rootx() cursor_y += textwidget.winfo_rooty() # leave some space cursor_y -= 5 cursor_height += 10 popup_width = settings.get('autocomplete_popup_width', int) popup_height = settings.get('autocomplete_popup_height', int) screen_width = textwidget.winfo_screenwidth() screen_height = textwidget.winfo_screenheight() # don't go off the screen to the right, leave space between popup # and right side of window x = min(screen_width - popup_width - 10, cursor_x) if cursor_y + cursor_height + popup_height < screen_height: # it fits below cursor, put it there y = cursor_y + cursor_height else: # put it above cursor instead. If it doesn't fit there either, # then y is also negative and the user has a tiny screen or a # huge popup size. y = cursor_y - popup_height return f'{popup_width}x{popup_height}+{x}+{y}'
def test_add_option_and_get_and_set(cleared_global_settings): settings.add_option('how_many_foos', 123) settings.add_option('bar_message', 'hello') assert settings.get('how_many_foos', int) == 123 assert settings.get('bar_message', str) == 'hello' settings.set('how_many_foos', 456) settings.set('bar_message', 'bla') assert settings.get('how_many_foos', int) == 456 assert settings.get('bar_message', str) == 'bla'
def test_add_option_and_get_and_set(cleared_global_settings): settings.add_option("how_many_foos", 123) settings.add_option("bar_message", "hello") assert settings.get("how_many_foos", int) == 123 assert settings.get("bar_message", str) == "hello" settings.set_("how_many_foos", 456) settings.set_("bar_message", "bla") assert settings.get("how_many_foos", int) == 456 assert settings.get("bar_message", str) == "bla"
def test_wrong_type(cleared_global_settings): settings.add_option('magic_message', 'bla') with pytest.raises( dacite.exceptions.WrongTypeError, match=r'wrong value type .* should be "int" instead of .* "str"'): settings.get('magic_message', int) with pytest.raises( dacite.exceptions.WrongTypeError, match=r'wrong value type .* should be "str" instead of .* "int"'): settings.set('magic_message', 123)
def set_font(self, junk: object = None) -> None: font = (settings.get('font_family', str), round(settings.get('font_size', int) / 3), ()) how_to_show_tab = ' ' * self._tab.settings.get('indent_size', int) # tkinter doesn't provide a better way to do font stuff than stupid # font object self.config( tabs=self.tk.call('font', 'measure', font, how_to_show_tab)) self.tag_config('sel', font=font) self._update_vast()
def set_font(self, junk: object = None) -> None: self.tag_config( "sel", font=(settings.get("font_family", str), round(settings.get("font_size", int) / 3), ()), ) textwidget.config_tab_displaying(self, self._tab.settings.get( "indent_size", int), tag="sel") self._update_vast()
def test_unknown_option_in_settings_file(cleared_global_settings): load_from_json_string('{"foo": "custom", "unknown": "hello"}') with pytest.raises(KeyError): settings.get('foo', str) settings.add_option('foo', 'default') assert settings.get('foo', str) == 'custom' settings.set('foo', 'default') assert settings.get('foo', str) == 'default' assert save_and_read_file() == {'unknown': 'hello'}
def test_unknown_option_in_settings_file(cleared_global_settings): load_from_json_string('{"foo": "custom", "unknown": "hello"}') with pytest.raises(KeyError): settings.get("foo", str) settings.add_option("foo", "default") assert settings.get("foo", str) == "custom" settings.set_("foo", "default") assert settings.get("foo", str) == "default" assert save_and_read_file() == {"unknown": "hello"}
def _update_row(self, info: pluginloader.PluginInfo) -> None: if info.came_with_porcupine: how_it_got_installed = "Came with Porcupine" else: how_it_got_installed = "You installed this" disable_list = settings.get('disabled_plugins', List[str]) if info.status == pluginloader.Status.DISABLED_BY_SETTINGS and info.name not in disable_list: message = "Will be enabled upon restart" elif info.status != pluginloader.Status.DISABLED_BY_SETTINGS and info.name in disable_list: message = "Will be disabled upon restart" else: message = { pluginloader.Status.ACTIVE: "Active", pluginloader.Status.DISABLED_BY_SETTINGS: "Disabled", pluginloader.Status.DISABLED_ON_COMMAND_LINE: "Disabled on command line", pluginloader.Status.IMPORT_FAILED: "Importing failed", pluginloader.Status.SETUP_FAILED: "Setup failed", pluginloader.Status.CIRCULAR_DEPENDENCY_ERROR: "Circular dependency", }[info.status] self._treeview.item(info.name, values=(info.name, how_it_got_installed, message))
def on_style_changed(junk: object = None) -> None: style = styles.get_style_by_name(settings.get("pygments_style", str)) bg = style.background_color # yes, style.default_style can be '#rrggbb', '' or nonexistent # this is undocumented # # >>> from pygments.styles import * # >>> [getattr(get_style_by_name(name), 'default_style', '???') # ... for name in get_all_styles()] # ['', '', '', '', '', '', '???', '???', '', '', '', '', # '???', '???', '', '#cccccc', '', '', '???', '', '', '', '', # '#222222', '', '', '', '???', ''] fg = getattr(style, "default_style", "") or utils.invert_color(bg) if callback is None: assert isinstance(widget, tkinter.Text) widget.config( foreground=fg, background=bg, insertbackground=fg, # cursor color selectforeground=bg, selectbackground=fg, ) else: callback(fg, bg)
def _on_select(self, junk: object = None) -> None: [plugin_name] = self._treeview.selection() [info] = [ info for info in pluginloader.plugin_infos if info.name == plugin_name ] if info.status == pluginloader.Status.IMPORT_FAILED: text = f"Importing the plugin failed.\n\n{info.error}" elif info.status == pluginloader.Status.SETUP_FAILED: text = f"The plugin's setup() function failed.\n\n{info.error}" elif info.status == pluginloader.Status.CIRCULAR_DEPENDENCY_ERROR: assert info.error is not None text = info.error else: text = get_docstring(f'porcupine.plugins.{info.name}') # get rid of single newlines text = re.sub(r'(.)\n(.)', r'\1 \2', text) self._title_label.config(text=plugin_name) self._description.config(state='normal') self._description.delete('1.0', 'end') self._description.insert('1.0', text) self._description.config(state='disabled') disable_list = settings.get('disabled_plugins', List[str]) self._enable_disable_button.config( state='normal', text=("Enable" if plugin_name in disable_list else "Disable"))
def _on_select(self, junk: object = None) -> None: infos = self._get_selected_infos() disable_list = settings.get("disabled_plugins", List[str]) if len(infos) == 1: info = infos[0] if info.status == pluginloader.Status.IMPORT_FAILED: text = f"Importing the plugin failed.\n\n{info.error}" elif info.status == pluginloader.Status.SETUP_FAILED: text = f"The plugin's setup() function failed.\n\n{info.error}" elif info.status == pluginloader.Status.CIRCULAR_DEPENDENCY_ERROR: assert info.error is not None text = info.error else: text = get_docstring(f"porcupine.plugins.{info.name}") # get rid of single newlines text = re.sub(r"(.)\n(.)", r"\1 \2", text) self._title_label.config(text=info.name) self._set_description(text) else: self._title_label.config(text="") self._set_description(f"{len(infos)} plugins selected.") self.enable_button.config(state=("normal" if any( info.name in disable_list for info in infos) else "disabled")) self.disable_button.config(state=("normal" if any( info.name not in disable_list for info in infos) else "disabled"))
def get_filetype_for_tab(tab: tabs.FileTab) -> FileType: if tab.path is None: return filetypes[settings.get('default_filetype', str)] # FIXME: this may read the shebang from the file, but the file # might not be saved yet because save_as() sets self.path # before saving, and that's when this runs return guess_filetype(tab.path)
def check_if_it_finished() -> None: if thread.is_alive(): get_main_window().after(200, check_if_it_finished) return var = tkinter.StringVar(value=settings.get("pygments_style", str)) def settings2var(event: tkinter.Event[tkinter.Misc]) -> None: var.set(settings.get("pygments_style", str)) def var2settings(*junk: str) -> None: settings.set_("pygments_style", var.get()) # this doesn't recurse infinitely because <<SettingChanged:bla>> # gets generated only when the setting actually changes get_tab_manager().bind("<<SettingChanged:pygments_style>>", settings2var, add=True) var.trace_add("write", var2settings) for style_name in style_names: fg, bg = get_colors(style_name) menubar.get_menu("Color Styles").add_radiobutton( label=style_name, value=style_name, variable=var, foreground=fg, background=bg, # swapped colors activeforeground=bg, activebackground=fg, )
def setup() -> None: style = ttkthemes.ThemedStyle() settings.add_option("ttk_theme", style.theme_use()) var = tkinter.StringVar() for name in sorted(style.get_themes()): menubar.get_menu("Ttk Themes").add_radiobutton(label=name, value=name, variable=var) # Connect style and var var.trace_add("write", lambda *junk: style.set_theme(var.get())) # Connect var and settings get_main_window().bind( "<<SettingChanged:ttk_theme>>", lambda event: var.set(settings.get("ttk_theme", str)), add=True, ) var.set(settings.get("ttk_theme", str)) var.trace_add("write", lambda *junk: settings.set_("ttk_theme", var.get()))
def test_no_json_file(cleared_global_settings): assert not settings._get_json_path().exists() settings._load_from_file() settings.add_option('foo', 'default') settings.set('foo', 'custom') assert not settings._get_json_path().exists() settings.reset_all() assert settings.get('foo', str) == 'default'
def test_no_json_file(cleared_global_settings): assert not settings._get_json_path().exists() settings._load_from_file() settings.add_option("foo", "default") settings.set_("foo", "custom") assert not settings._get_json_path().exists() settings.reset_all() assert settings.get("foo", str) == "default"
def on_style_changed(self, junk: object = None) -> None: style = styles.get_style_by_name(settings.get("pygments_style", str)) infos = dict(iter(style)) # iterating is documented for tokentype in [token.Error, token.Name.Exception]: if tokentype in infos: for key in ["bgcolor", "color", "border"]: if infos[tokentype][key] is not None: self.frame.config(bg=("#" + infos[tokentype][key])) return # stupid fallback self.frame.config(bg="red")
def __init__(self, manager: TabManager, content: str = '', path: Optional[pathlib.Path] = None, filetype: Optional[Dict[str, Any]] = None) -> None: super().__init__(manager) self._save_hash: Optional[str] = None if path is None: self._path = None else: self._path = path.resolve() self.settings = settings.Settings(self, '<<TabSettingChanged:{}>>') self.settings.add_option('pygments_lexer', pygments.lexers.TextLexer, type=pygments.lexer.LexerMeta, converter=_import_lexer_class) self.settings.add_option('tabs2spaces', True) self.settings.add_option('indent_size', 4) self.settings.add_option('encoding', 'utf-8') self.settings.add_option('line_ending', settings.get('default_line_ending', settings.LineEnding), converter=settings.LineEnding.__getitem__) # we need to set width and height to 1 to make sure it's never too # large for seeing other widgets self.textwidget = textwidget.MainText(self, width=1, height=1, wrap='none', undo=True) self.textwidget.pack(side='left', fill='both', expand=True) self.textwidget.bind('<<ContentChanged>>', self._update_title, add=True) if content: self.textwidget.insert('1.0', content) self.textwidget.edit_reset() # reset undo/redo self.bind('<<PathChanged>>', self._update_status, add=True) self.textwidget.bind('<<CursorMoved>>', self._update_status, add=True) self.scrollbar = ttk.Scrollbar(self.right_frame) self.scrollbar.pack(side='right', fill='y') self.textwidget.config(yscrollcommand=self.scrollbar.set) self.scrollbar.config(command=self.textwidget.yview) self.mark_saved() self._update_title() self._update_status()
def on_style_changed(self, junk: object = None) -> None: style = pygments.styles.get_style_by_name(settings.get('pygments_style', str)) infos = dict(iter(style)) # iterating is documented for tokentype in [pygments.token.Error, pygments.token.Name.Exception]: if tokentype in infos: for key in ['bgcolor', 'color', 'border']: if infos[tokentype][key] is not None: self.frame.config(bg=('#' + infos[tokentype][key])) return # stupid fallback self.frame.config(bg='red')
def _toggle_enabled(self) -> None: [plugin_name] = self._treeview.selection() [info] = [ info for info in pluginloader.plugin_infos if info.name == plugin_name ] disabled = set(settings.get('disabled_plugins', List[str])) disabled ^= {plugin_name} settings.set('disabled_plugins', list(disabled)) self._update_row(info) self._on_select() self._update_plz_restart_label()
def setup() -> None: style = ttkthemes.ThemedStyle() # https://github.com/RedFantom/ttkthemes/issues/6 # this does what style.theme_use() should do default_theme = style.tk.eval('return $ttk::currentTheme') settings.add_option('ttk_theme', default_theme) var = tkinter.StringVar() var.trace_add('write', functools.partial(on_theme_changed, style, var)) var.set(settings.get('ttk_theme', str)) for name in sorted(style.get_themes()): menubar.get_menu("Ttk Themes").add_radiobutton(label=name, value=name, variable=var)
def change_font_size(how: Literal['bigger', 'smaller', 'reset']) -> None: if how == 'reset': settings.reset('font_size') return size = settings.get('font_size', int) if how == 'bigger': size += 1 else: size -= 1 if size < 3: return settings.set('font_size', size)
def change_font_size(how: Literal["bigger", "smaller", "reset"]) -> None: if how == "reset": settings.reset("font_size") return size = settings.get("font_size", int) if how == "bigger": size += 1 else: size -= 1 if size < 3: return settings.set_("font_size", size)
def test_remember_panedwindow_positions(toplevel): pw = ttk.PanedWindow(toplevel, orient="horizontal") settings.remember_divider_positions(pw, "pw_dividers", [123]) pw.pack(fill="both", expand=True) pw.add(ttk.Label(pw, text="aaaaaaaaaaa")) pw.add(ttk.Label(pw, text="bbbbbbbbbbb")) pw.update() assert pw.sashpos(0) == 123 pw.sashpos(0, 456) pw.event_generate("<ButtonRelease-1>") # happens after user drags pane assert settings.get("pw_dividers", List[int]) == [456]
def test_reset(cleared_global_settings): load_from_json_string( '{"foo": "custom", "bar": "custom", "unknown": "hello"}') settings.add_option('foo', 'default') settings.add_option('bar', 'default') settings.add_option('baz', 'default') assert settings.get('foo', str) == 'custom' assert settings.get('bar', str) == 'custom' assert settings.get('baz', str) == 'default' settings.reset('bar') assert settings.get('foo', str) == 'custom' assert settings.get('bar', str) == 'default' assert settings.get('baz', str) == 'default' assert save_and_read_file() == {'foo': 'custom', 'unknown': 'hello'} settings.reset_all() assert settings.get('foo', str) == 'default' assert settings.get('bar', str) == 'default' assert settings.get('baz', str) == 'default' assert save_and_read_file() == {} # even unknown options go away
def test_reset(cleared_global_settings): load_from_json_string( '{"foo": "custom", "bar": "custom", "unknown": "hello"}') settings.add_option("foo", "default") settings.add_option("bar", "default") settings.add_option("baz", "default") assert settings.get("foo", str) == "custom" assert settings.get("bar", str) == "custom" assert settings.get("baz", str) == "default" settings.reset("bar") assert settings.get("foo", str) == "custom" assert settings.get("bar", str) == "default" assert settings.get("baz", str) == "default" assert save_and_read_file() == {"foo": "custom", "unknown": "hello"} settings.reset_all() assert settings.get("foo", str) == "default" assert settings.get("bar", str) == "default" assert settings.get("baz", str) == "default" assert save_and_read_file() == {} # even unknown options go away
def _set_enabled(self, they_become_enabled: bool) -> None: infos = self._get_selected_infos() disabled = set(settings.get("disabled_plugins", List[str])) if they_become_enabled: disabled -= {info.name for info in infos} else: disabled |= {info.name for info in infos} settings.set_("disabled_plugins", list(disabled)) for info in infos: if info.name not in disabled and pluginloader.can_setup_while_running( info): pluginloader.setup_while_running(info) self._update_row(info) self._on_select() self._update_plz_restart_label()
def import_plugins(disabled_on_command_line: List[str]): assert not _mutable_plugin_infos _mutable_plugin_infos.extend( PluginInfo( name=name, came_with_porcupine=_did_plugin_come_with_porcupine(finder), status=Status.LOADING, module=None, setup_before=set(), setup_after=set(), error=None, ) for finder, name, is_pkg in pkgutil.iter_modules(plugin_paths) if not name.startswith('_')) for info in _mutable_plugin_infos: # If it's disabled in settings and on command line, then status is set # to DISABLED_BY_SETTINGS. This makes more sense for the user of the # plugin manager dialog. if info.name in settings.get('disabled_plugins', List[str]): info.status = Status.DISABLED_BY_SETTINGS continue if info.name in disabled_on_command_line: info.status = Status.DISABLED_ON_COMMAND_LINE continue log.debug(f"trying to import porcupine.plugins.{info.name}") start = time.time() try: info.module = importlib.import_module( f'porcupine.plugins.{info.name}') info.setup_before = set(getattr(info.module, 'setup_before', [])) info.setup_after = set(getattr(info.module, 'setup_after', [])) except Exception: log.exception(f"can't import porcupine.plugins.{info.name}") info.status = Status.IMPORT_FAILED info.error = traceback.format_exc() continue duration = time.time() - start log.debug("imported porcupine.plugins.%s in %.3f milliseconds", info.name, duration * 1000)
def _style_changed(self, junk: object = None) -> None: # http://pygments.org/docs/formatterdevelopment/#styles # all styles seem to yield all token types when iterated over, # so we should always end up with the same tags configured style = styles.get_style_by_name(settings.get("pygments_style", str)) for tokentype, infodict in style: # this doesn't use underline and border # i don't like random underlines in my code and i don't know # how to implement the border with tkinter self.textwidget.tag_config( str(tokentype), font=self._fonts[(infodict["bold"], infodict["italic"])], # empty string resets foreground foreground=("" if infodict["color"] is None else "#" + infodict["color"]), background=("" if infodict["bgcolor"] is None else "#" + infodict["bgcolor"]), ) # make sure that the selection tag takes precedence over our # token tag self.textwidget.tag_lower(str(tokentype), "sel")