def __init__(self, ui, plugin, pageview): Dialog.__init__( self, ui, _('Insert Symbol'), # T: Dialog title button=(_('_Insert'), 'gtk-ok'), # T: Button label defaultwindowsize=(350, 400)) self.plugin = plugin self.pageview = pageview if not plugin.symbols: plugin.load_file() self.textentry = InputEntry() self.vbox.pack_start(self.textentry, False) # TODO make this iconview single-click model = gtk.ListStore(str, str) # text, shortcut self.iconview = gtk.IconView(model) self.iconview.set_text_column(0) self.iconview.set_column_spacing(0) self.iconview.set_row_spacing(0) if gtk.gtk_version >= (2, 12, 0): self.iconview.set_property('has-tooltip', True) self.iconview.connect('query-tooltip', self.on_query_tooltip) self.iconview.connect('item-activated', self.on_activated) self.vbox.add(ScrolledWindow(self.iconview)) button = gtk.Button(stock=gtk.STOCK_EDIT) button.connect('clicked', self.on_edit) self.action_area.add(button) self.action_area.reorder_child(button, 0) self.load_symbols()
def __init__(self, pageview, preferences): """Initialize the Input Box with options.""" Dialog.__init__( self, pageview, _('Search in Zotero'), # T: Dialog title button=_('_GO'), # T: Button label defaultwindowsize=(350, 200)) self.pageview = pageview self.textentry = InputEntry() self.vbox.pack_start(self.textentry, False, True, 0) self.preferences = preferences first = None options = [ "Search in Title, Author and Date", "Search in All Fields and Tags", "Search Everywhere" ] for text in options: self.radio = Gtk.RadioButton.new_with_mnemonic_from_widget( first, text) if not first: first = self.radio self.vbox.pack_start(self.radio, False, True, 0) self.radio.show()
def __init__(self, tasksview, opener, properties, uistate): GObject.GObject.__init__(self) self.uistate = uistate self.uistate.setdefault('only_show_act', False) self.uistate.setdefault('show_flatlist', False) self.task_list = TaskListTreeView( tasksview, opener, _parse_task_labels(properties['labels']), nonactionable_tags=_parse_task_labels( properties['nonactionable_tags']), filter_actionable=self.uistate['only_show_act'], tag_by_page=properties['tag_by_page'], use_workweek=properties['use_workweek'], compact=True, flatlist=self.uistate['show_flatlist'], ) self.task_list.connect('populate-popup', self.on_populate_popup) self.task_list.set_headers_visible(True) self.connectto(properties, 'changed', self.on_properties_changed) self.filter_entry = InputEntry(placeholder_text=_( 'Filter')) # T: label for filtering/searching tasks self.filter_entry.set_icon_to_clear() filter_cb = DelayedCallback( 500, lambda o: self.task_list.set_filter(self.filter_entry.get_text())) self.filter_entry.connect('changed', filter_cb) self.pack_start(ScrolledWindow(self.task_list), True, True, 0) self.pack_end(self.filter_entry, False, True, 0)
def __init__(self, parent, file): title = _('Rename file') # T: dialog title Dialog.__init__(self, parent, title) assert isinstance(file, LocalFile) and not isinstance(file, LocalFolder) self.old_file = file self.new_file = file # Add field for entering new filename. self.txt_filename = InputEntry() self.txt_filename.set_text(self.old_file.basename) self.txt_filename.set_activates_default( True) # Make ENTER key press trigger the OK button. self.txt_filename.connect("changed", self.do_validate, parent) # Add field for showing hints when an error occurs (e.g. filename already exists). self.txt_error = Gtk.Label() self.txt_error.set_visible(False) # Add ok button. self.btn_ok = self.get_widget_for_response( response_id=Gtk.ResponseType.OK) self.btn_ok.set_can_default(True) self.btn_ok.grab_default() self.btn_ok.set_sensitive(False) # Configure dialog. self.set_modal(True) self.set_default_size(380, 100) self.vbox.pack_start(self.txt_filename, False, True, 0) self.vbox.pack_start(self.txt_error, False, True, 0) # Set focus to search field self.txt_filename.grab_focus()
def __init__(self, tasksview, opener, preferences, uistate): gtk.VBox.__init__(self) self.uistate = uistate self.uistate.setdefault('only_show_act', False) self.uistate.setdefault('show_flatlist', False) column_layout=TaskListTreeView.COMPACT_COLUMN_LAYOUT_WITH_DUE \ if preferences['with_due'] else TaskListTreeView.COMPACT_COLUMN_LAYOUT self.task_list = TaskListTreeView( tasksview, opener, _parse_task_labels(preferences['labels']), nonactionable_tags=_parse_task_labels( preferences['nonactionable_tags']), filter_actionable=self.uistate['only_show_act'], tag_by_page=preferences['tag_by_page'], use_workweek=preferences['use_workweek'], column_layout=column_layout, flatlist=self.uistate['show_flatlist'], ) self.task_list.connect('populate-popup', self.on_populate_popup) self.task_list.set_headers_visible(True) self.filter_entry = InputEntry(placeholder_text=_( 'Filter')) # T: label for filtering/searching tasks self.filter_entry.set_icon_to_clear() filter_cb = DelayedCallback( 500, lambda o: self.task_list.set_filter(self.filter_entry.get_text())) self.filter_entry.connect('changed', filter_cb) self.pack_start(ScrolledWindow(self.task_list)) self.pack_end(self.filter_entry, False)
def __init__(self, parent, plugin, pageview): Dialog.__init__( self, parent, _('Insert Symbol'), # T: Dialog title button=_('_Insert'), # T: Button label defaultwindowsize=(350, 400)) self.plugin = plugin self.pageview = pageview if not plugin.symbols: plugin.load_file() self.textentry = InputEntry() self.vbox.pack_start(self.textentry, False, True, 0) model = Gtk.ListStore(str, str) # text, shortcut self.iconview = Gtk.IconView(model) self.iconview.set_text_column(0) self.iconview.set_column_spacing(0) self.iconview.set_row_spacing(0) self.iconview.set_property('has-tooltip', True) self.iconview.set_property('activate-on-single-click', True) self.iconview.connect('query-tooltip', self.on_query_tooltip) self.iconview.connect('item-activated', self.on_activated) swindow = ScrolledWindow(self.iconview) self.vbox.pack_start(swindow, True, True, 0) button = Gtk.Button.new_with_mnemonic(_('_Edit')) # T: Button label button.connect('clicked', self.on_edit) self.action_area.add(button) self.action_area.reorder_child(button, 0) self.load_symbols()
def __init__(self, window): Dialog.__init__( self, window, _('Search'), # T: Dialog title buttons=gtk.BUTTONS_CLOSE, help='Help:Searching', defaultwindowsize=(400, 300)) self.app_window = window hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) hbox.pack_start(gtk.Label(_('Search') + ': '), False) # T: input label self.query_entry = InputEntry() hbox.add(self.query_entry) self.search_button = gtk.Button(stock=gtk.STOCK_FIND) hbox.pack_start(self.search_button, False) if gtk.gtk_version >= (2, 20) \ and gtk.pygtk_version >= (2, 22): # update in pygtk was later self.spinner = gtk.Spinner() hbox.pack_start(self.spinner, False) else: self.spinner = None self.cancel_button = gtk.Button(stock=gtk.STOCK_STOP) hbox.pack_start(self.cancel_button, False) self._set_state(self.READY) help_text = _('For advanced search you can use operators like\n' 'AND, OR and NOT. See the help page for more details.' ) # T: help text for the search dialog if gtk.gtk_version >= (2, 12) \ and gtk.pygtk_version >= (2, 12): self.query_entry.set_tooltip_text(help_text) else: tooltips = gtk.Tooltips() tooltips.set_tip(self.query_entry, help_text) self.namespacecheckbox = gtk.CheckButton( _('Limit search to the current page and sub-pages')) # T: checkbox option in search dialog self.vbox.pack_start(self.namespacecheckbox, False) # TODO advanced query editor # TODO checkbox _('Match c_ase') # TODO checkbox _('Whole _word') self.results_treeview = SearchResultsTreeView(self.app_window) self.vbox.add(ScrolledWindow(self.results_treeview)) self.search_button.connect_object('clicked', self.__class__._search, self) self.cancel_button.connect_object('clicked', self.__class__._cancel, self) self.query_entry.connect_object('activate', self.__class__._search, self)
def __init__(self, widget, notebook, page, navigation): Dialog.__init__( self, widget, _('Search'), # T: Dialog title buttons=Gtk.ButtonsType.CLOSE, help='Help:Searching', defaultwindowsize=(400, 300)) self.page = page hbox = Gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False, True, 0) search_label = Gtk.Label.new_with_mnemonic(_('_Search') + ': ') hbox.pack_start(search_label, False, True, 0) # T: input label self.query_entry = InputEntry() hbox.add(self.query_entry) search_label.set_mnemonic_widget(self.query_entry) self.search_button = Gtk.Button.new_with_mnemonic( _('_Find')) # T: Button label hbox.pack_start(self.search_button, False, True, 0) self.spinner = Gtk.Spinner() hbox.pack_start(self.spinner, False, True, 0) self.cancel_button = Gtk.Button.new_with_mnemonic( _('_Cancel')) # T: Button label hbox.pack_start(self.cancel_button, False, True, 0) self._set_state(self.READY) help_text = _('For advanced search you can use operators like\n' 'AND, OR and NOT. See the help page for more details.' ) # T: help text for the search dialog self.query_entry.set_tooltip_text(help_text) self.namespacecheckbox = Gtk.CheckButton.new_with_mnemonic( _('_Limit search to the current page and sub-pages')) # T: checkbox option in search dialog if page is not None: self.vbox.pack_start(self.namespacecheckbox, False, True, 0) # TODO advanced query editor # TODO checkbox _('Match c_ase') # TODO checkbox _('Whole _word') self.results_treeview = SearchResultsTreeView(notebook, navigation) self.vbox.pack_start(ScrolledWindow(self.results_treeview), True, True, 0) self.search_button.connect_object('clicked', self.__class__._search, self) self.cancel_button.connect_object('clicked', self.__class__._cancel, self) self.query_entry.connect_object('activate', self.__class__._search, self)
def __init__(self, parent): Dialog.__init__(self, parent, _('Insert Code Block')) # T: dialog title self.result = (None, None, None) self.uistate.define(id=String(None)) self.uistate.define(lang=String(None)) self.uistate.define(line_numbers=Boolean(True)) grid = Gtk.Grid() grid.set_column_spacing(5) grid.set_row_spacing(5) label = Gtk.Label(_('Syntax') + ':') # T: input label grid.add(label) self.combobox = Gtk.ComboBox.new_with_model_and_entry(self.init_combobox_model()) self.combobox.set_entry_text_column(0) entry = self.combobox.get_child() entry.set_activates_default(True) # Pressing enter will activate the default button (here: ok-button) completion = Gtk.EntryCompletion() completion.set_model(self.init_autocomplete_model()) completion.set_text_column(0) completion.set_minimum_key_length(0) entry.set_completion(completion) defaultlang = self.init_default_language() if defaultlang: entry.set_text(defaultlang) self.combobox.connect("changed", self.on_combobox_changed) grid.attach(self.combobox, 1, 0, 1, 1) label = Gtk.Label(_('Id') + ':') # T: input label for object ID grid.attach(label, 0, 1, 1, 1) self.entry = InputEntry() grid.attach(self.entry, 1, 1, 1, 1) self.checkbox = Gtk.CheckButton(_('Display line numbers')) # T: input checkbox self.checkbox.set_active(self.uistate['line_numbers']) grid.attach(self.checkbox, 1, 2, 1, 1) self.vbox.add(grid) # Set ok button as default. self.btn_ok = self.get_widget_for_response(response_id=Gtk.ResponseType.OK) self.btn_ok.set_can_default(True) self.btn_ok.grab_default() self.btn_ok.set_sensitive(defaultlang is not None)
class TaskListWidget(Gtk.VBox, TaskListWidgetMixin, WindowSidePaneWidget): title = _('Tas_ks') # T: tab label for side pane def __init__(self, tasksview, opener, properties, with_due, uistate): GObject.GObject.__init__(self) self.uistate = uistate self.uistate.setdefault('only_show_act', False) self.uistate.setdefault('show_flatlist', False) column_layout=TaskListTreeView.COMPACT_COLUMN_LAYOUT_WITH_DUE \ if with_due else TaskListTreeView.COMPACT_COLUMN_LAYOUT self.task_list = TaskListTreeView( tasksview, opener, _parse_task_labels(properties['labels']), nonactionable_tags=_parse_task_labels( properties['nonactionable_tags']), filter_actionable=self.uistate['only_show_act'], tag_by_page=properties['tag_by_page'], use_workweek=properties['use_workweek'], column_layout=column_layout, flatlist=self.uistate['show_flatlist'], ) self.task_list.connect('populate-popup', self.on_populate_popup) self.task_list.set_headers_visible(True) self.connectto(properties, 'changed', self.on_properties_changed) self.filter_entry = InputEntry(placeholder_text=_( 'Filter')) # T: label for filtering/searching tasks self.filter_entry.set_icon_to_clear() filter_cb = DelayedCallback( 500, lambda o: self.task_list.set_filter(self.filter_entry.get_text())) self.filter_entry.connect('changed', filter_cb) self.pack_start(ScrolledWindow(self.task_list), True, True, 0) self.pack_end(self.filter_entry, False, True, 0) def on_properties_changed(self, properties): self.task_list.update_properties( task_labels=_parse_task_labels(properties['labels']), nonactionable_tags=_parse_task_labels( properties['nonactionable_tags']), tag_by_page=properties['tag_by_page'], use_workweek=properties['use_workweek'], )
def __init__(self, ui, plugin): Dialog.__init__(self, ui, _('Insert Symbol'), # T: Dialog title button=(_('_Insert'), 'gtk-ok'), # T: Button label defaultwindowsize=(350, 400) ) self.plugin = plugin self.textentry = InputEntry() self.vbox.pack_start(self.textentry, False) # TODO make this iconview single-click model = gtk.ListStore(str, str) # text, shortcut self.iconview = gtk.IconView(model) self.iconview.set_text_column(0) self.iconview.set_column_spacing(0) self.iconview.set_row_spacing(0) if gtk.gtk_version >= (2, 12, 0): self.iconview.set_property('has-tooltip', True) self.iconview.connect('query-tooltip', self.on_query_tooltip) self.iconview.connect('item-activated', self.on_activated) self.vbox.add(ScrolledWindow(self.iconview)) button = gtk.Button(stock=gtk.STOCK_EDIT) button.connect('clicked', self.on_edit) self.action_area.add(button) self.action_area.reorder_child(button, 0) self.load_symbols()
def __init__(self, plugin): if ui_environment['platform'] == 'maemo': defaultsize = (800, 480) else: defaultsize = (900, 500) Dialog.__init__(self, plugin.ui, _('Qda Codes'), # T: dialog title buttons=gtk.BUTTONS_CLOSE, help=':Plugins:Qda Codes', defaultwindowsize=defaultsize) self.plugin = plugin if ui_environment['platform'] == 'maemo': self.resize(800, 480) # Force maximum dialog size under maemo, otherwise # we'll end with a too small dialog and no way to resize it hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) self.hpane = HPaned() self.uistate.setdefault('hpane_pos', 75) self.hpane.set_position(self.uistate['hpane_pos']) self.vbox.add(self.hpane) # Code list self.qda_codes = QdaCodesTreeView(self.ui, plugin) self.qda_codes.set_headers_visible(True) # Fix for maemo self.hpane.add2(ScrolledWindow(self.qda_codes)) # Tag list self.tag_list = TagListTreeView(self.qda_codes) self.hpane.add1(ScrolledWindow(self.tag_list)) # Filter input hbox.pack_start(gtk.Label(_('Filter') + ': '), False) # T: Input label filter_entry = InputEntry() filter_entry.set_icon_to_clear() hbox.pack_start(filter_entry, False) filter_cb = DelayedCallback(500, lambda o: self.qda_codes.set_filter(filter_entry.get_text())) filter_entry.connect('changed', filter_cb) # Statistics label # self.statistics_label = gtk.Label() # hbox.pack_end(self.statistics_label, False) export_button = gtk.Button(_('_Export') ) export_button.connect_object('clicked', self.qda_codes.get_data_as_page, self.qda_codes ) hbox.pack_end(export_button, False ) def on_qdacodes_changed(o): self.qda_codes.refresh() self.tag_list.refresh(self.qda_codes) # set_statistics() callback = DelayedCallback(10, on_qdacodes_changed) # Don't really care about the delay, but want to # make it less blocking - should be async preferably # now it is at least on idle self.connectto(plugin, 'qdacodes-changed', callback)
def __init__(self, ui): Dialog.__init__(self, ui, _('Search'), # T: Dialog title buttons=gtk.BUTTONS_CLOSE, help='Help:Searching', defaultwindowsize=(400, 300) ) hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) hbox.pack_start(gtk.Label(_('Search')+': '), False) # T: input label self.query_entry = InputEntry() hbox.add(self.query_entry) self.search_button = gtk.Button(stock=gtk.STOCK_FIND) hbox.pack_start(self.search_button, False) if gtk.gtk_version >= (2, 20) \ and gtk.pygtk_version >= (2, 22): # update in pygtk was later self.spinner = gtk.Spinner() hbox.pack_start(self.spinner, False) else: self.spinner = None self.cancel_button = gtk.Button(stock=gtk.STOCK_STOP) hbox.pack_start(self.cancel_button, False) self._set_state(self.READY) help_text = _( 'For advanced search you can use operators like\n' 'AND, OR and NOT. See the help page for more details.' ) # T: help text for the search dialog if gtk.gtk_version >= (2, 12) \ and gtk.pygtk_version >= (2, 12): self.query_entry.set_tooltip_text(help_text) else: tooltips = gtk.Tooltips() tooltips.set_tip(self.query_entry, help_text) self.namespacecheckbox = gtk.CheckButton(_('Limit search to the current page and sub-pages')) # T: checkbox option in search dialog self.vbox.pack_start(self.namespacecheckbox, False) # TODO advanced query editor # TODO checkbox _('Match c_ase') # TODO checkbox _('Whole _word') self.results_treeview = SearchResultsTreeView(self.ui) self.vbox.add(ScrolledWindow(self.results_treeview)) self.search_button.connect_object('clicked', self.__class__._search, self) self.cancel_button.connect_object('clicked', self.__class__._cancel, self) self.query_entry.connect_object('activate', self.__class__._search, self)
def __init__(self, window, index_ext, preferences): Dialog.__init__( self, window, _('Task List'), # T: dialog title buttons=gtk.BUTTONS_CLOSE, help=':Plugins:Task List', defaultwindowsize=(550, 400)) self.preferences = preferences self.index_ext = index_ext hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) self.hpane = HPaned() self.uistate.setdefault('hpane_pos', 75) self.hpane.set_position(self.uistate['hpane_pos']) self.vbox.add(self.hpane) # Task list self.uistate.setdefault('only_show_act', False) opener = window.get_resource_opener() self.task_list = TaskListTreeView( self.index_ext, opener, filter_actionable=self.uistate['only_show_act'], tag_by_page=preferences['tag_by_page'], use_workweek=preferences['use_workweek']) self.task_list.set_headers_visible(True) # Fix for maemo self.hpane.add2(ScrolledWindow(self.task_list)) # Tag list self.tag_list = TagListTreeView(self.index_ext, self.task_list) self.hpane.add1(ScrolledWindow(self.tag_list)) # Filter input hbox.pack_start(gtk.Label(_('Filter') + ': '), False) # T: Input label filter_entry = InputEntry() filter_entry.set_icon_to_clear() hbox.pack_start(filter_entry, False) filter_cb = DelayedCallback( 500, lambda o: self.task_list.set_filter(filter_entry.get_text())) filter_entry.connect('changed', filter_cb) # Dropdown with options - TODO #~ menu = gtk.Menu() #~ showtree = gtk.CheckMenuItem(_('Show _Tree')) # T: menu item in options menu #~ menu.append(showtree) #~ menu.append(gtk.SeparatorMenuItem()) #~ showall = gtk.RadioMenuItem(None, _('Show _All Items')) # T: menu item in options menu #~ showopen = gtk.RadioMenuItem(showall, _('Show _Open Items')) # T: menu item in options menu #~ menu.append(showall) #~ menu.append(showopen) #~ menubutton = MenuButton(_('_Options'), menu) # T: Button label #~ hbox.pack_start(menubutton, False) self.act_toggle = gtk.CheckButton(_('Only Show Actionable Tasks')) # T: Checkbox in task list self.act_toggle.set_active(self.uistate['only_show_act']) self.act_toggle.connect( 'toggled', lambda o: self.task_list.set_filter_actionable(o.get_active())) hbox.pack_start(self.act_toggle, False) # Statistics label self.statistics_label = gtk.Label() hbox.pack_end(self.statistics_label, False) def set_statistics(): total, stats = self.task_list.get_statistics() text = ngettext('%i open item', '%i open items', total) % total # T: Label for statistics in Task List, %i is the number of tasks text += ' (' + '/'.join(map(str, stats)) + ')' self.statistics_label.set_text(text) set_statistics() def on_tasklist_changed(o): self.task_list.refresh() self.tag_list.refresh(self.task_list) set_statistics() callback = DelayedCallback(10, on_tasklist_changed) # Don't really care about the delay, but want to # make it less blocking - should be async preferably # now it is at least on idle self.connectto(index_ext, 'tasklist-changed', callback)
class InsertCodeBlockDialog(Dialog): def __init__(self, parent): Dialog.__init__(self, parent, _('Insert Code Block')) # T: dialog title self.result = (None, None, None) self.uistate.define(id=String(None)) self.uistate.define(lang=String(None)) self.uistate.define(line_numbers=Boolean(True)) grid = Gtk.Grid() grid.set_column_spacing(5) grid.set_row_spacing(5) label = Gtk.Label(_('Syntax') + ':') # T: input label grid.add(label) self.combobox = Gtk.ComboBox.new_with_model_and_entry(self.init_combobox_model()) self.combobox.set_entry_text_column(0) entry = self.combobox.get_child() entry.set_activates_default(True) # Pressing enter will activate the default button (here: ok-button) completion = Gtk.EntryCompletion() completion.set_model(self.init_autocomplete_model()) completion.set_text_column(0) completion.set_minimum_key_length(0) entry.set_completion(completion) defaultlang = self.init_default_language() if defaultlang: entry.set_text(defaultlang) self.combobox.connect("changed", self.on_combobox_changed) grid.attach(self.combobox, 1, 0, 1, 1) label = Gtk.Label(_('Id') + ':') # T: input label for object ID grid.attach(label, 0, 1, 1, 1) self.entry = InputEntry() grid.attach(self.entry, 1, 1, 1, 1) self.checkbox = Gtk.CheckButton(_('Display line numbers')) # T: input checkbox self.checkbox.set_active(self.uistate['line_numbers']) grid.attach(self.checkbox, 1, 2, 1, 1) self.vbox.add(grid) # Set ok button as default. self.btn_ok = self.get_widget_for_response(response_id=Gtk.ResponseType.OK) self.btn_ok.set_can_default(True) self.btn_ok.grab_default() self.btn_ok.set_sensitive(defaultlang is not None) def init_default_language(self): for lang in sorted(LANGUAGES, key=lambda k: k.lower()): if LANGUAGES[lang] == self.uistate['lang']: return lang return None def init_combobox_model(self): menu = {} for l in sorted(LANGUAGES, key=lambda k: k.lower()): key = l[0].upper() if not key in menu: menu[key] = [] menu[key].append(l) model = Gtk.TreeStore(str) for key in sorted(menu): iter = model.append(None, [key]) for lang in menu[key]: model.append(iter, [lang]) return model def init_autocomplete_model(self): store = Gtk.TreeStore(str) for lang in sorted(LANGUAGES, key=lambda k: k.lower()): store.append(None, [lang]) return store def on_combobox_changed(self, widget): """ Checks whether the text entry in combobox is valid and enables/disables the ok-button. """ self.btn_ok = self.get_widget_for_response(response_id=Gtk.ResponseType.OK) self.btn_ok.set_sensitive(widget.get_child().get_text() in LANGUAGES) def do_response_ok(self): if self.combobox.get_child().get_text() in LANGUAGES: self.uistate['lang'] = LANGUAGES[self.combobox.get_child().get_text()] self.uistate['id'] = self.entry.get_text() self.uistate['line_numbers'] = self.checkbox.get_active() self.result = (self.uistate['id'], self.uistate['lang'], self.uistate['line_numbers']) return True else: return False # no syntax selected
def __init__(self, notebookinfo=None, port=8080, public=True, **opts): '''Constructor @param notebookinfo: the notebook location @param port: the http port to serve on @param public: allow connections to the server from other computers - if C{False} can only connect from localhost @param opts: options for L{WWWInterface.__init__()} ''' GObject.GObject.__init__(self) self.set_title('Zim - ' + _('Web Server')) # T: Window title self.set_border_width(10) self.interface_opts = opts self.httpd = None self._source_id = None # Widgets self.status_label = Gtk.Label() self.status_label.set_markup('<i>' + _('Server not started') + '</i>') # T: Status in web server gui self.start_button = IconButton('gtk-media-play') self.start_button.connect('clicked', lambda o: self.start()) self.stop_button = IconButton('gtk-media-stop') self.stop_button.connect('clicked', lambda o: self.stop()) self.stop_button.set_sensitive(False) self.link_button = Gtk.LinkButton('') self.link_button.set_sensitive(False) self.notebookcombobox = NotebookComboBox(current=notebookinfo) self.open_button = IconButton('gtk-index') self.open_button.connect('clicked', lambda *a: NotebookDialog(self).run()) self.portentry = Gtk.SpinButton() self.portentry.set_numeric(True) self.portentry.set_range(80, 10000) self.portentry.set_increments(1, 80) self.portentry.set_value(port) self.public_checkbox = Gtk.CheckButton.new_with_mnemonic( _('Allow public access')) # T: Checkbox in web server gui self.public_checkbox.set_active(public) self.templatecombobox = Gtk.ComboBoxText.new() template_names = [name for name, _ in list_templates('html')] for name in template_names: self.templatecombobox.append_text(name) self.templatecombobox.set_active(template_names.index('Default')) self.auth_checkbox = Gtk.CheckButton.new_with_mnemonic( _('Require authentication')) # T: checkbox in dialog for starting webserver self.username_input = InputEntry() self.password_input = InputEntry() self.password_input.set_visibility(False) # Build the interface vbox = Gtk.VBox() self.add(vbox) hbox = Gtk.HBox(spacing=12) hbox.pack_start(self.start_button, False, True, 0) hbox.pack_start(self.stop_button, False, True, 0) hbox.pack_start(self.status_label, False, True, 0) vbox.pack_start(hbox, False, False, 0) table = input_table_factory(( (_('Notebook'), self.notebookcombobox, self.open_button), # T: Field in web server gui (_('Port'), self.portentry), # T: Field in web server gui for HTTP port (e.g. port 80) (_('Template'), self.templatecombobox), # T: Field in web server gui for webpage template self.public_checkbox, self.auth_checkbox, (_('Username'), self.username_input), # T: Field in web server gui (_('Password'), self.password_input) # T: Field in web server gui )) vbox.pack_start(table, False, False, 0) if self.link_button: hbox = Gtk.HBox() hbox.pack_end(self.link_button, False, True, 0) vbox.pack_start(hbox, False, False, 0)
class ServerWindow(Gtk.Window): def __init__(self, notebookinfo=None, port=8080, public=True, **opts): '''Constructor @param notebookinfo: the notebook location @param port: the http port to serve on @param public: allow connections to the server from other computers - if C{False} can only connect from localhost @param opts: options for L{WWWInterface.__init__()} ''' GObject.GObject.__init__(self) self.set_title('Zim - ' + _('Web Server')) # T: Window title self.set_border_width(10) self.interface_opts = opts self.httpd = None self._source_id = None # Widgets self.status_label = Gtk.Label() self.status_label.set_markup('<i>' + _('Server not started') + '</i>') # T: Status in web server gui self.start_button = IconButton('gtk-media-play') self.start_button.connect('clicked', lambda o: self.start()) self.stop_button = IconButton('gtk-media-stop') self.stop_button.connect('clicked', lambda o: self.stop()) self.stop_button.set_sensitive(False) self.link_button = Gtk.LinkButton('') self.link_button.set_sensitive(False) self.notebookcombobox = NotebookComboBox(current=notebookinfo) self.open_button = IconButton('gtk-index') self.open_button.connect('clicked', lambda *a: NotebookDialog(self).run()) self.portentry = Gtk.SpinButton() self.portentry.set_numeric(True) self.portentry.set_range(80, 10000) self.portentry.set_increments(1, 80) self.portentry.set_value(port) self.public_checkbox = Gtk.CheckButton.new_with_mnemonic( _('Allow public access')) # T: Checkbox in web server gui self.public_checkbox.set_active(public) self.templatecombobox = Gtk.ComboBoxText.new() template_names = [name for name, _ in list_templates('html')] for name in template_names: self.templatecombobox.append_text(name) self.templatecombobox.set_active(template_names.index('Default')) self.auth_checkbox = Gtk.CheckButton.new_with_mnemonic( _('Require authentication')) # T: checkbox in dialog for starting webserver self.username_input = InputEntry() self.password_input = InputEntry() self.password_input.set_visibility(False) # Build the interface vbox = Gtk.VBox() self.add(vbox) hbox = Gtk.HBox(spacing=12) hbox.pack_start(self.start_button, False, True, 0) hbox.pack_start(self.stop_button, False, True, 0) hbox.pack_start(self.status_label, False, True, 0) vbox.pack_start(hbox, False, False, 0) table = input_table_factory(( (_('Notebook'), self.notebookcombobox, self.open_button), # T: Field in web server gui (_('Port'), self.portentry), # T: Field in web server gui for HTTP port (e.g. port 80) (_('Template'), self.templatecombobox), # T: Field in web server gui for webpage template self.public_checkbox, self.auth_checkbox, (_('Username'), self.username_input), # T: Field in web server gui (_('Password'), self.password_input) # T: Field in web server gui )) vbox.pack_start(table, False, False, 0) if self.link_button: hbox = Gtk.HBox() hbox.pack_end(self.link_button, False, True, 0) vbox.pack_start(hbox, False, False, 0) def open_notebook(self, notebook): '''Sets the notebook in the combobox This method is called by the NotebookDialog when a notebook is opened. ''' self.notebookcombobox.set_notebook(notebook) def start(self): # Start server try: uri = self.notebookcombobox.get_notebook() if uri: notebook, x = build_notebook(NotebookInfo(uri)) if not notebook: return else: return if not notebook.index.is_uptodate: for info in notebook.index.update_iter(): #logger.info('Indexing %s', info) pass # TODO meaningful info for above message port = int(self.portentry.get_value()) public = self.public_checkbox.get_active() self.interface_opts[ 'template'] = self.templatecombobox.get_active_text() require_auth = self.auth_checkbox.get_active() auth_creds = None if require_auth: auth_creds = (self.username_input.get_text(), self.password_input.get_text()) self.httpd = make_server(notebook, port, public, auth_creds=auth_creds, **self.interface_opts) if sys.platform == 'win32': # GObject io watch conflicts with socket use on windows.. # idle handler uses a bit to much CPU for my taste, # timeout every 0.5 sec is better self.httpd.timeout = 0.1 # 100 ms self._source_id = GObject.timeout_add(500, self.do_serve_on_poll) else: self.httpd.timeout = 3 # if no response after 3 sec, drop it self._source_id = GObject.io_add_watch( self.httpd.fileno(), GObject.IO_IN | GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP | GObject.IO_PRI, # any event.. self.do_serve_on_io) logger.info("Serving HTTP on %s port %i...", self.httpd.server_name, self.httpd.server_port) except Exception as error: ErrorDialog(self, error).run() return # Update UI self.notebookcombobox.set_sensitive(False) self.portentry.set_sensitive(False) self.public_checkbox.set_sensitive(False) self.templatecombobox.set_sensitive(False) self.open_button.set_sensitive(False) self.start_button.set_sensitive(False) self.stop_button.set_sensitive(True) self.auth_checkbox.set_sensitive(False) self.username_input.set_sensitive(False) self.password_input.set_sensitive(False) self.status_label.set_markup('<i>' + _('Server started') + '</i>') # T: Status in web server gui #if self.public_checkbox.get_active(): # url = 'http://%s:%i' % (self.httpd.server_name, self.httpd.server_port) #else: # url = 'http://localhost:%i' % self.httpd.server_port url = 'http://localhost:%i' % self.httpd.server_port if self.link_button: self.link_button.set_uri(url) self.link_button.set_label(url) self.link_button.set_sensitive(True) def do_serve_on_io(self, fd, event): try: if event & GObject.IO_HUP: self.stop() raise Exception('Socket disconnected') else: self.httpd.handle_request() except: logger.exception('Exception while handling IO request:') return True # keep event running def do_serve_on_poll(self): self.httpd.handle_request() return True # keep event running def stop(self): # Stop server logger.debug('Stop server') if self._source_id is not None: GObject.source_remove(self._source_id) self._source_id = None if self.httpd: self.httpd.socket.close() # There is also a httpd.server_close(), but undocumented (!?) self.httpd = None # Update UI self.status_label.set_markup('<i>' + _('Server stopped') + '</i>') # T: Status in web server gui if self.link_button: self.link_button.set_sensitive(False) self.notebookcombobox.set_sensitive(True) self.portentry.set_sensitive(True) self.public_checkbox.set_sensitive(True) self.templatecombobox.set_sensitive(True) self.open_button.set_sensitive(True) self.stop_button.set_sensitive(False) self.start_button.set_sensitive(True) self.auth_checkbox.set_sensitive(True) self.username_input.set_sensitive(True) self.password_input.set_sensitive(True)
def instantsearch(self): # init self.cached_titles = [] # self.menu = defaultdict(_MenuItem) self.lastQuery = "" # previous user input self.queryO = None self.caret = {'pos': 0, 'altPos': 0, 'text': ""} # cursor position self.originalPage = self.window.page.name # we return here after escape self.selection = None if not self.plugin.preferences['isCached']: # reset last search results State.reset() self.menuPage = None self.isClosed = False self.lastPage = None # preferences self.title_match_char = self.plugin.preferences['title_match_char'] self.start_search_length = self.plugin.preferences[ 'start_search_length'] self.keystroke_delay = self.plugin.preferences['keystroke_delay'] self.open_when_unique = self.plugin.preferences['open_when_unique'] # building quick title cache def build(start=""): if hasattr(self.window.notebook, 'pages'): o = self.window.notebook.pages else: # for Zim 0.66- o = self.window.notebook.index for s in o.list_pages(Path(start or ":")): start2 = (start + ":" if start else "") + s.basename self.cached_titles.append((start2, start2.lower())) build(start2) build() # Gtk self.gui = Dialog(self.window, _('Search'), buttons=None, defaultwindowsize=(300, -1)) self.gui.resize(300, 100) # reset size self.inputEntry = InputEntry() self.inputEntry.connect('key_press_event', self.move) self.inputEntry.connect( 'changed', self.change) # self.change is needed by GObject or something self.gui.vbox.pack_start(self.inputEntry, expand=False, fill=True, padding=0) self.labelObject = Gtk.Label(label=('')) self.labelObject.set_size_request(300, -1) self.gui.vbox.pack_start(self.labelObject, expand=False, fill=True, padding=0) # gui geometry px, py = self.window.get_position() pw, ph = self.window.get_size() x, y = self.gui.get_position() if self.plugin.preferences[ 'position'] == InstantsearchPlugin.POSITION_RIGHT: self.gui.move((pw - 300), 0) elif self.plugin.preferences[ 'position'] == InstantsearchPlugin.POSITION_CENTER: self.gui.resize(300, 100) self.gui.move(px + (pw / 2) - 150, py + (ph / 2) - 250) else: raise AttributeError("Instant search: Wrong position preference.") self.gui.show_all() self.labelVar = "" self.timeout = "" self.timeoutOpenPage = None
def instantsearch(self): #init self.cached_titles = [] #self.menu = defaultdict(_MenuItem) self.lastQuery = "" # previous user input self.queryO = None self.caret = {'pos':0, 'altPos':0, 'text':""} # cursor position self.originalPage = self.window.ui.page.name # we return here after escape self.selection = None if not self.plugin.preferences['isCached']: # reset last search results State.reset() self.menuPage = None self.isClosed = False self.lastPage = None # preferences self.title_match_char = self.plugin.preferences['title_match_char'] self.start_search_length = self.plugin.preferences['start_search_length'] self.keystroke_delay = self.plugin.preferences['keystroke_delay'] self.open_when_unique = self.plugin.preferences['open_when_unique'] # building quick title cache def build(start = ""): if hasattr(self.window.ui.notebook, 'pages'): o = self.window.ui.notebook.pages else: # for Zim 0.66- o = self.window.ui.notebook.index for s in o.list_pages(Path(start or ":")): start2 = (start + ":" if start else "") + s.basename self.cached_titles.append((start2, start2.lower())) build(start2) build() # Gtk self.gui = Dialog(self.window.ui, _('Search'), buttons=None, defaultwindowsize=(300, -1)) self.gui.resize(300, 100) # reset size self.inputEntry = InputEntry() self.inputEntry.connect('key_press_event', self.move) self.inputEntry.connect('changed', self.change) # self.change is needed by GObject or something self.gui.vbox.pack_start(self.inputEntry, False) self.labelObject = gtk.Label(('')) self.labelObject.set_usize(300, -1) self.gui.vbox.pack_start(self.labelObject, False) #gui geometry px, py = self.window.get_position() pw, ph = self.window.get_size() x, y = self.gui.get_position() if self.plugin.preferences['position'] == InstantsearchPlugin.POSITION_RIGHT: self.gui.move((pw-300), 0) elif self.plugin.preferences['position'] == InstantsearchPlugin.POSITION_CENTER: self.gui.resize(300, 100) self.gui.move(px + (pw / 2) - 150, py + (ph / 2) - 250) else: raise AttributeError("Instant search: Wrong position preference.") self.gui.show_all() self.labelVar = "" self.timeout = "" self.timeoutOpenPage = None
class InstantSearchMainWindowExtension(MainWindowExtension): gui: "Dialog" state: "State" def __init__(self, plugin, window): super().__init__(plugin, window) self.timeout = None self.timeout_open_page = None # will open page after keystroke delay self.timeout_open_page_preview = None # will open page after keystroke delay self.cached_titles = None self.last_query = None self.query_o = None self.caret = None self.original_page = None self.original_history = None self.selection = None self.menu_page = None self.is_closed = None self.last_page = self.last_page_preview = None self.label_object = None self.input_entry = None self.label_preview = None self.preview_pane = None self._last_update = 0 # preferences State.title_match_char = self.plugin.preferences['title_match_char'] State.start_search_length = self.plugin.preferences[ 'start_search_length'] self.keystroke_delay_open = self.plugin.preferences[ 'keystroke_delay_open'] self.keystroke_delay = self.plugin.preferences['keystroke_delay'] # noinspection PyArgumentList,PyUnresolvedReferences @action(_('_Instant search'), accelerator='<ctrl>e') # T: menu item def instant_search(self): # init self.cached_titles = [] self.last_query = "" # previous user input self.query_o = None self.caret = SimpleNamespace(pos=0, text="", stick=False) # cursor position self.original_page = self.window.page.name # we return here after escape self.original_history = list(self.window.history.uistate["list"]) self.selection = None if not self.plugin.preferences['is_cached']: # reset last search results State.reset() self.menu_page = None self.is_closed = False self.last_page = None # building quick title cache def build(start=""): o = self.window.notebook.pages for s in o.list_pages(Path(start or ":")): start2 = (start + ":" if start else "") + s.basename self.cached_titles.append((start2, start2.lower())) build(start2) build() # Gtk self.gui = Dialog(self.window, _('Search'), buttons=None, defaultwindowsize=(300, -1)) self.gui.resize(300, 100) # reset size self.input_entry = InputEntry() self.input_entry.connect('key_press_event', self.move) self.input_entry.connect( 'changed', self.change) # self.change is needed by GObject or something self.gui.vbox.pack_start(self.input_entry, expand=False, fill=True, padding=0) # noinspection PyArgumentList self.label_object = Gtk.Label(label='') self.label_object.set_size_request(300, -1) self.gui.vbox.pack_start(self.label_object, expand=False, fill=True, padding=0) # preview pane self.label_preview = Gtk.Label(label='...loading...') # not sure if this has effect, longer lines without spaces still make window inflate self.label_preview.set_line_wrap(True) self.label_preview.set_xalign(0) # align to the left self.preview_pane = Gtk.VBox() self.preview_pane.pack_start(self.label_preview, False, False, 5) self.window.pageview.pack_start(self.preview_pane, False, False, 5) # gui geometry self.geometry(init=True) self.gui.show_all() def geometry(self, init=False, repeat=True, force=False): if repeat and not init: # I do not know how to catch callback when result list's width is final, so we align several times [ GObject.timeout_add( x, lambda: self.geometry(repeat=False, force=force)) for x in (30, 50, 70, 400) ] # it is not worthy we continue now because often the Gtk redraw is delayed which would mean # the Dialog dimensions change twice in a row return px, py = self.window.get_position() pw, ph = self.window.get_size() if init: x, y = None, None w, h = 300, 100 else: x, y = self.gui.get_position() w, h = self.gui.get_allocated_width( ), self.gui.get_allocated_height() if self.plugin.preferences[ 'position'] == InstantSearchPlugin.POSITION_RIGHT: x2, y2 = pw - w, 0 elif self.plugin.preferences[ 'position'] == InstantSearchPlugin.POSITION_CENTER: x2, y2 = px + (pw / 2) - w / 2, py + (ph / 2) - 250 else: raise AttributeError("Instant search: Wrong position preference.") if init or x != x2 or force: self.gui.resize(300, 100) self.gui.move(x2, y2) def title(self, title=""): self.gui.set_title("Search " + title) def change(self, _): # widget, event,text if self.timeout: GObject.source_remove(self.timeout) self.timeout = None q = self.input_entry.get_text() if q == self.last_query: return if q == State.title_match_char: return if q and q[-1] == "∀": # easter egg: debug option for zim --standalone q = q[:-1] import ipdb ipdb.set_trace() self.state = State.set_current(q) if not self.state.is_finished: if self.start_search(): self.process_menu() else: # search completed before # update the results if a page has been modified meanwhile # (not if something got deleted in the notebook #16 ) self.start_search() self.check_last() self.sout_menu() self.last_query = q def start_search(self): """ Search string has certainly changed. We search in indexed titles and/or we start fulltext search. :rtype: True if no other search is needed and we may output the menu immediately. """ query = self.state.query menu = self.state.menu if not query: return self.process_menu() # show for now results of title search # 'te' matches these page titles: 'test' or 'Journal:test' or 'foo test' or 'foo (test)' sub_queries = [ re.compile(r"(^|:|\s|\()?" + q, re.IGNORECASE) for q in query.split(" ") ] def in_query(txt): """ False if any part of the query does not match. If the query is longer >3 characters: * +10 for every query part that matches a title part beginning Ex: query 'te' -> +3 for these page titles: 'test' or 'Journal:test' or 'foo test' or 'foo (test)' * +1 for every query part Ex: query 'st' -> +1 for those page titles If the query is shorter <=3 characters: +10 for every query part that matches a title part beginning 'te' for 'test' False otherwise ('st' for 'test') so that you do not end up messed with page titles, after writing a single letter. """ arr = (q.search(txt) for q in sub_queries) try: if len(query) <= 3: # raises if subquery m does not match or is not at a page chunk beginning return sum(10 if m.group(1) is not None else None for m in arr) else: # raises if subquery m does not match return sum(10 if m.group(1) is not None else 1 for m in arr) except (AttributeError, TypeError ): # one of the sub_queries is not part of the page title return False # we loop either all cached page titles or menu that should be built from previous superset-query menu it = ((x, x.lower()) for x in list(menu)) if menu else self.cached_titles for path, path_low in it: # quick search in titles score = in_query(path_low) if score: # 'te' matches 'test' or 'Journal:test' etc # "foo" in "foo:bar", but not in "bar" # when looping "foo:bar", page "foo" receives +1 for having a subpage if all(q in path.lower() for q in query) \ and any(q not in path.lower().split(":")[-1] for q in query): menu[":".join(path.split( ":")[:-1])].bonus += 1 # 1 point for having a subpage # Normally, zim search gives 11 points bonus if the search-string appears in the titles. # If we are ignoring sub-pages, the search "foo" will match only page "journal:foo", # but not "journal:foo:subpage" # (and score of the parent page will get slightly higher by 1.) # However, if there are occurrences of the string in the fulltext of the subpage, # subpage remains in the result, but gets bonus only 2 points (not 11). # But internal zim search is now disabled. # menu[path].bonus = -11 else: # 10 points for title (zim default) (so that it gets displayed before search finishes) menu[ path].bonus += score # will be added to score (score will be reset) # if score > 9, it means this might be priority match, not fulltext header search # ex "te" for "test" is priority, whereas "st" is just fulltext menu[path].in_title = True if score > 9 else False menu[path].path = path # menu[path].sure = True if self.state.page_title_only: return True else: if not self.state.previous or len( query) == State.start_search_length: # quickly show page title search results before longer fulltext search is ready # Either there is no previous state – query might have been copied into input # or the query is finally long enough to start fulltext search. # It is handy to show out filtered page names before because # it is often use case to jump to queries matched in page names. self.process_menu(ignore_geometry=True) self.title("..") self.timeout = GObject.timeout_add( self.keystroke_delay, self.start_zim_search) # ideal delay between keystrokes def start_zim_search(self): """ Starts search for the input. """ self.title("...") if self.timeout: GObject.source_remove(self.timeout) self.timeout = None self.query_o = Query(self.state.query) # it should be quicker to find the string, if we provide this subset from last time # (in the case we just added a letter, so that the subset gets smaller) # last_sel = self.selection if self.is_subset and self.state.previous and self.state.previous.is_finished # else None selection = self.selection = SearchSelection(self.window.notebook) state = self.state # this is a thread, so that self.state might change before search finishes # internal search disabled - it was way too slower # selection.search(self.query_o, selection=last_sel, callback=self._search_callback(state)) # self._update_results(selection, state, force=True) # self.title("....") # fulltext external search # Loop either all .txt files in the notebook or narrow the search with a previous state if state.previous and state.previous.is_finished and state.previous.matching_files is not None: paths_set = state.previous.matching_files # see below paths_cached_set = (p for p in files_set if p in InstantSearchPlugin.file_cache) else: paths_set = (f for f in pathlib.Path( str(self.window.notebook.folder)).rglob("*.txt") if f.is_file()) # see below paths_cached_set = (p for p in InstantSearchPlugin.file_cache) state.matching_files = [] # This cached search takes about 60 ms, so I let it commented. # However on HDD disks this may boost performance. # We may do an option: "empty cache immediately after close (default)", # "search cache first and then do the fresh search (HDD)" # "use cache always (empties cache after Zim restart)" # "empty cache after 5 minutes" # and then prevent to clear the cache in .close(). # Or rather we may read file mtime and re-read if only it has been changed since last search. # if not InstantSearchPlugin.file_cache_fresh: # # Cache might not be fresh but since it is quick, perform quick non-fresh-cached search # # and then do a fresh search. If we are lucky enough, results will not change. # # using temporary selection so that files will not received double points for both cached and fresh loop # selection_temp = SearchSelection(self.window.notebook) # self.start_external_search(selection_temp, state, paths_cached_set) # InstantSearchPlugin.file_cache_fresh = True # InstantSearchPlugin.file_cache.clear() self.start_external_search(selection, state, paths_set) state.is_finished = True # for item in list(state.menu): # remove all the items that we didnt encounter during the search # if not state.menu[item].sure: # del state.menu[item] if state == self.state: self.check_last() self.process_menu(state=state) self.title() def start_external_search(self, selection, state: "State", paths): """ Zim internal search is not able to find out text with markup. Ex: 'economical' is not recognized as 'economi**cal**' (however highlighting works great), as 'economi[[inserted link]]cal' as 'any text with [[http://economical.example.com|link]]' This fulltext search loops all .txt files in the notebook directory and tries to recognize the patterns. """ # divide query to independent words "foo economical" -> "foo", "economical", page has to contain both # strip markup: **bold**, //italic//, __underline__, ''verbatim'', ~~strike through~~ # matches query "economi**cal**" def letter_split(q): """ Every letter is divided by a any-formatting-match-group and escaped. 'foo.' -> 'f[*/'_~]o[*/'_~]o[*/'_~]\\.' """ return r"[*/'_~]*".join((re.escape(c) for c in list(q))) sub_queries = state.query.split(" ") # regex to identify in all sub_queries present in the text queries = [ re.compile(letter_split(q), re.IGNORECASE) for q in sub_queries ] # regex to identify the very query is present exact_query = re.compile(letter_split( state.query), re.IGNORECASE) if len(sub_queries) > 1 else None # regex to count the number of the sub_queries present and to optionally add information about header used header_queries = [ re.compile("(\n=+ .*)?" + letter_split(q), re.IGNORECASE) for q in sub_queries ] # regex to identify inner link contents link = re.compile( r"\[\[(.*?)\]\]", re.IGNORECASE) # matches all links "economi[[inserted link]]cal" for p in paths: if p not in InstantSearchPlugin.file_cache: s = p.read_text() # strip header if s.startswith('Content-Type: text/x-zim-wiki'): # XX will that work on Win? # I should use more general separator IMHO in the whole file rather than '\n'. s = s[s.find("\n\n"):] InstantSearchPlugin.file_cache[p] = s else: s = InstantSearchPlugin.file_cache[p] matched_links = [] def matched_link(match): matched_links.append(match.group(1)) return "" # pull out links "economi[[inserted link]]cal" -> "economical" + "inserted link" txt_body = link.sub(matched_link, s) txt_links = "".join(matched_links) if all( query.search(txt_body) or query.search(txt_links) for query in queries): path = self.window.notebook.layout.map_file(File(str(p)))[0] # score = header order * 3 + body match count * 1 # if there are '=' equal chars before the query, it is header. The bigger number, the bigger header. # Header 5 corresponds to 3 points, Header 1 to 7 points. score = sum([ len(m.group(1)) * 3 if m.group(1) else 1 for q in header_queries for m in q.finditer(txt_body) ]) if exact_query: # there are sub-queries, we favourize full-match score += 50 * len(exact_query.findall(txt_body)) # noinspection PyProtectedMember # score might be zero because we are not re-checking against txt_links matches selection._count_score(path, score or 1) state.matching_files.append(p) self._update_results(selection, state, force=True) def check_last(self): """ opens the page if there is only one option in the menu """ if len(self.state.menu ) == 1 and self.plugin.preferences['open_when_unique']: self._open_page(Path(list(self.state.menu)[0]), exclude_from_history=False) self.close() elif not len(self.state.menu): self._open_original() def _search_callback(self, state): def _(results, _path): if results is not None: # we finish the search even if another search is running. # If returned False, the search would be cancelled self._update_results(results, state) while Gtk.events_pending(): Gtk.main_iteration() return True return _ def _update_results(self, results, state: "State", force=False): """ This method may run many times, due to the _update_results, which are updated many times, the results are appearing one by one. However, if called earlier than 0.2 s, ignored. Measures: If every callback would be counted, it takes 3500 ms to build a result set. If callbacks earlier than 0.6 s -> 2300 ms, 0.3 -> 2600 ms, 0.1 -> 2800 ms. """ if not force and time( ) < self._last_update + 0.2: # if update callback called earlier than 200 ms, ignore return self._last_update = time() changed = False for option in results.scores: if option.name not in state.menu or ( state.menu[option.name].bonus < 0 and state.menu[option.name].score == 0): changed = True o: _MenuItem = state.menu[option.name] # if not o.sure: # o.sure = True # changed = True o.score = results.scores[option] # includes into options if changed: # we added a page self.process_menu(state=state, sort=False) else: pass def process_menu(self, state=None, sort=True, ignore_geometry=False): """ Sort menu and generate items and sout menu. """ if state is None: state = self.state if sort: state.items = sorted( state.menu, reverse=True, key=lambda item: (state.menu[item].in_title, state.menu[item].score + state. menu[item].bonus, -item.count(":"), item)) else: # when search results are being updated, it's good when the order doesnt change all the time. # So that the first result does not become for a while 10th and then become first back. state.items = sorted( state.menu, reverse=True, key=lambda item: (state.menu[item].in_title, -state.menu[item].last_order)) # I do not know why there are items with score 0 if internal Zim search used state.items = [ item for item in state.items if (state.menu[item].score + state.menu[item].bonus) > 0 ] if state == self.state: self.sout_menu(ignore_geometry=ignore_geometry) def sout_menu(self, display_immediately=False, caret_move=None, ignore_geometry=False): """ Displays menu and handles caret position. """ if self.timeout_open_page: GObject.source_remove(self.timeout_open_page) self.timeout_open_page = None if self.timeout_open_page_preview: GObject.source_remove(self.timeout_open_page_preview) self.timeout_open_page_preview = None # caret: # by default stays at position 0 # If moved to a page, it keeps the page. # If moved back to position 0, stays there. if caret_move is not None: if caret_move == 0: self.caret.pos = 0 else: self.caret.pos += caret_move self.caret.stick = self.caret.pos != 0 elif self.state.items and self.caret.stick: # identify current caret position, depending on the text self.caret.pos = next((i for i, item in enumerate(self.state.items) if item == self.caret.text), 0) # treat possible caret deflection if self.caret.pos < 0: # place the caret to the beginning or the end of list self.caret.pos = 0 elif self.caret.pos >= len(self.state.items): self.caret.pos = 0 if caret_move == 1 else len( self.state.items) - 1 text = [] i = 0 for item in self.state.items: score = self.state.menu[item].score + self.state.menu[item].bonus self.state.menu[item].last_order = i pieces = item.split(":") pieces[-1] = f"<b>{pieces[-1]}</b>" s = ":".join(pieces) if i == self.caret.pos: self.caret.text = item # caret is at this position # text += f'→ {s} ({score}) {"" if self.state.menu[item].sure else "?"}\n' text.append(f'→ {s} ({score})') else: # text += f'{s} ({score}) {"" if self.state.menu[item].sure else "?"}\n' text.append(f'{s} ({score})') i += 1 text = "No result" if not text and self.state.is_finished else "\n".join( text) self.label_object.set_markup(text) self.menu_page = Path( self.caret.text if len(self.state.items) else self.original_page) if not display_immediately: if self.plugin.preferences[ 'preview_mode'] != InstantSearchPlugin.PREVIEW_ONLY: self.timeout_open_page = GObject.timeout_add( self.keystroke_delay_open, self._open_page, self.menu_page) # ideal delay between keystrokes if self.plugin.preferences[ 'preview_mode'] != InstantSearchPlugin.FULL_ONLY: self.timeout_open_page_preview = GObject.timeout_add( self.keystroke_delay, self._open_page_preview, self.menu_page) # ideal delay between keystrokes else: self._open_page(self.menu_page) # we force here geometry to redraw because often we end up with "No result" page that is very tall # because of a many records just hidden if not ignore_geometry: self.geometry(force=True) def move(self, widget, event): """ Move caret up and down. Enter to confirm, Esc closes search.""" key_name = Gdk.keyval_name(event.keyval) # handle basic caret movement moves = { "Up": -1, "ISO_Left_Tab": -1, "Down": 1, "Tab": 1, "Page_Up": -10, "Page_Down": 10 } if key_name in moves: self.sout_menu(display_immediately=False, caret_move=moves[key_name]) elif key_name in ("Home", "End"): if event.state & Gdk.ModifierType.CONTROL_MASK or event.state & Gdk.ModifierType.SHIFT_MASK: # Ctrl/Shift+Home jumps to the query input text start return if key_name == "Home": # Home jumps at the result list start self.sout_menu(display_immediately=False, caret_move=0) widget.emit_stop_by_name("key-press-event") else: self.sout_menu(display_immediately=False, caret_move=float("inf")) widget.emit_stop_by_name("key-press-event") # confirm or cancel elif key_name == "KP_Enter" or key_name == "Return": self._open_page(self.menu_page, exclude_from_history=False) self.close() elif key_name == "Escape": self._open_original() self.is_closed = True # few more timeouts are on the way probably self.close() return def close(self): """ Safely (closes gets called when hit Enter) """ if not self.is_closed: # if hit Esc, GTK has already emitted close itself self.is_closed = True self.gui.emit("close") # remove preview pane and show current text editor self._hide_preview() self.preview_pane.destroy() InstantSearchPlugin.file_cache.clear( ) # until next search, pages might change InstantSearchPlugin.file_cache_fresh = False def _open_original(self): self._open_page(Path(self.original_page)) # we already have HistoryPath objects in the self.original_history, we cannot add them in the constructor # XX I do not know what is that good for hl = HistoryList([]) hl.extend(self.original_history) self.window.history.uistate["list"] = hl # noinspection PyProtectedMember def _open_page(self, page, exclude_from_history=True): """ Open page and highlight matches """ self._hide_preview() if self.timeout_open_page: # no delayed page will be open GObject.source_remove(self.timeout_open_page) self.timeout_open_page = None if self.timeout_open_page_preview: # no delayed preview page will be open GObject.source_remove(self.timeout_open_page_preview) self.timeout_open_page_preview = None # open page if page and page.name and page.name != self.last_page: self.last_page = page.name self.window.navigation.open_page(page) if exclude_from_history and list( self.window.history._history )[-1:][0].name != self.original_page: # there is no public API, so lets use protected _history instead self.window.history._history.pop() self.window.history._current = len( self.window.history._history) - 1 if not exclude_from_history and self.window.history.get_current( ).name is not page.name: # we insert the page to the history because it was likely to be just visited and excluded self.window.history.append(page) # Popup find dialog with same query if self.query_o: # and self.query_o.simple_match: string = self.state.query string = string.strip('*') # support partial matches if self.plugin.preferences['highlight_search']: # unfortunately, we can highlight single word only self.window.pageview.show_find(string.split(" ")[0], highlight=True) def _hide_preview(self): self.preview_pane.hide() # noinspection PyProtectedMember self.window.pageview._hack_hbox.show() def _open_page_preview(self, page): """ Open preview which is far faster then loading and building big parse trees into text editor buffer when opening page. """ # note: if the dialog is already closed, we do not want a preview to open, but page still can be open # (ex: after hitting Enter the dialog can close before opening the page) if self.timeout_open_page_preview: # no delayed preview page will be open, however self.timeout_open_page might be still running GObject.source_remove(self.timeout_open_page_preview) self.timeout_open_page_preview = None # it does not pose a problem if we re-load preview on the same page; # the query text might got another letter to highlight if page and not self.is_closed: # show preview pane and hide current text editor self.last_page_preview = page.name local_file = self.window.notebook.layout.map_page(page)[0] path = pathlib.Path(str(local_file)) if path in InstantSearchPlugin.file_cache: s = InstantSearchPlugin.file_cache[path] else: try: s = InstantSearchPlugin.file_cache[path] = local_file.read( ) except newfs.base.FileNotFoundError: s = f"page {page} has no content" # page has not been created yet lines = s.splitlines() # the file length is very small, prefer to not use preview here if self.plugin.preferences[ 'preview_mode'] != InstantSearchPlugin.PREVIEW_ONLY and len( lines) < 50: return self._open_page(page, exclude_from_history=True) self.label_preview.set_markup( self._get_preview_text(lines, self.state.query)) # shows GUI (hidden in self._hide_preview() self.preview_pane.show_all() # noinspection PyProtectedMember self.window.pageview._hack_hbox.hide() def _get_preview_text(self, lines, query): max_lines = 200 # check if the file is a Zim markup file and if so, skip header if lines[0] == 'Content-Type: text/x-zim-wiki': for i, line in enumerate(lines): if line == "": lines = lines[i + 1:] break if query.strip() == "": return "\n".join(line for line in lines[:max_lines]) # searching for "a" cannot match "&a", since markup_escape_text("&") -> "'" # Ignoring q == "b", it would interfere with multiple queries: # Ex: query "f b", text "foo", matched with "f" -> "<b>f</b>oo", matched with "b" -> "<<b>b</b>>f</<b>b</b>>" query_match = (re.compile("(" + re.escape(q) + ")", re.IGNORECASE) for q in query.split(" ") if q != "b") # too long lines caused strange Gtk behaviour – monitor brightness set to maximum, without any logged warning # so that I decided to put just extract of such long lines in preview # This regex matches query chunk in the line, prepends characters before and after. # When there should be the same query chunk after the first, it stops. # Otherwise, the second chunk might be halved and thus not highlighted. # Ex: query "test", text: "lorem ipsum text dolor text text sit amet consectetur" -> # ["ipsum text dolor ", "text ", "text sit amet"] (words "lorem" and "consectetur" are strip) line_extract = [ re.compile( "(.{0,80}" + re.escape(q) + "(?:(?!" + re.escape(q) + ").){0,80})", re.IGNORECASE) for q in query.split(" ") if q != "b" ] # grep some lines keep_all = not self.plugin.preferences["preview_short"] and len( lines) < max_lines lines_iter = iter(lines) chosen = [ next(lines_iter) ] # always include header as the first line, even if it does not contain the query for line in lines_iter: if len( chosen ) > max_lines: # file is too long which would result the preview to not be smooth break elif keep_all or any(q in line.lower() for q in query.split(" ")): # keep this line since it contains a query chunk if len(line) > 100: # however, this line is too long to display, try to extract query and its neighbourhood s = "...".join("...".join(q.findall(line)) for q in line_extract).strip(".") if not s: # no query chunk was find on this line, the keep_all is True for sure chosen.append(line[:100] + "...") else: chosen.append("..." + s + "...") else: chosen.append(line) if not keep_all or len(chosen) > max_lines: # note that query might not been found, ex: query "foo" would not find line with a bold 'o': "f**o**o" chosen.append("...") txt = markup_escape_text("\n".join(line for line in chosen)) # bold query chunks in the text for q in query_match: txt = q.sub(r"<b>\g<1></b>", txt) # preserve markup_escape_text entities # correct ex: '&a<b>m</b>p;' -> '&' if searching for 'm' bold_tag = re.compile("</?b>") broken_entity = re.compile("&[a-z]*<b[^;]*;") txt = broken_entity.sub(lambda m: bold_tag.sub("", m.group(0)), txt) return txt
def __init__(self, window, tasksview, preferences): Dialog.__init__( self, window, _('Task List'), # T: dialog title buttons=gtk.BUTTONS_CLOSE, help=':Plugins:Task List', defaultwindowsize=(550, 400)) self.preferences = preferences self.tasksview = tasksview hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) self.hpane = HPaned() self.uistate.setdefault('hpane_pos', 75) self.hpane.set_position(self.uistate['hpane_pos']) self.vbox.add(self.hpane) # Task list self.uistate.setdefault('only_show_act', False) self.uistate.setdefault('show_flatlist', False) self.uistate.setdefault('sort_column', 0) self.uistate.setdefault('sort_order', int(gtk.SORT_DESCENDING)) opener = window.get_resource_opener() task_labels = _parse_task_labels(preferences['labels']) nonactionable_tags = _parse_task_labels( preferences['nonactionable_tags']) self.task_list = TaskListTreeView( self.tasksview, opener, task_labels, nonactionable_tags=nonactionable_tags, filter_actionable=self.uistate['only_show_act'], tag_by_page=preferences['tag_by_page'], use_workweek=preferences['use_workweek'], flatlist=self.uistate['show_flatlist'], sort_column=self.uistate['sort_column'], sort_order=self.uistate['sort_order']) self.task_list.set_headers_visible(True) self.task_list.connect('populate-popup', self.on_populate_popup) self.hpane.add2(ScrolledWindow(self.task_list)) # Tag list self.tag_list = TagListTreeView(self.task_list, task_labels) self.hpane.add1(ScrolledWindow(self.tag_list)) # Filter input hbox.pack_start(gtk.Label(_('Filter') + ': '), False) # T: Input label filter_entry = InputEntry() filter_entry.set_icon_to_clear() hbox.pack_start(filter_entry, False) filter_cb = DelayedCallback( 500, lambda o: self.task_list.set_filter(filter_entry.get_text())) filter_entry.connect('changed', filter_cb) # TODO: use menu button here and add same options as in context menu # for filtering the list def on_show_active_toggle(o): active = self.act_toggle.get_active() if self.uistate['only_show_act'] != active: self.uistate['only_show_act'] = active self.task_list.set_filter_actionable(active) self.act_toggle = gtk.CheckButton(_('Only Show Active Tasks')) # T: Checkbox in task list - this options hides tasks that are not yet started self.act_toggle.set_active(self.uistate['only_show_act']) self.act_toggle.connect('toggled', on_show_active_toggle) self.uistate.connect( 'changed', lambda o: self.act_toggle.set_active(self.uistate[ 'only_show_act'])) hbox.pack_start(self.act_toggle, False) # Statistics label self.statistics_label = gtk.Label() hbox.pack_end(self.statistics_label, False) def set_statistics(): total = self.task_list.get_n_tasks() text = ngettext('%i open item', '%i open items', total) % total # T: Label for task List, %i is the number of tasks self.statistics_label.set_text(text) set_statistics() def on_tasklist_changed(o): self.task_list.refresh() self.tag_list.refresh(self.task_list) set_statistics() callback = DelayedCallback(10, on_tasklist_changed) # Don't really care about the delay, but want to # make it less blocking - should be async preferably # now it is at least on idle ### XXX HACK to get dependency to connect to ### -- no access to plugin, so can;t use get_extension() ## -- duplicat of this snippet in MainWindowExtension for e in window.ui.notebook.__zim_extension_objects__: if hasattr(e, 'indexer' ) and e.indexer.__class__.__name__ == 'TasksIndexer': self.connectto(e, 'tasklist-changed', callback) break else: raise AssertionError, 'Could not find tasklist notebook extension'
class FileRenameDialog(Dialog): """ A dialog for renaming a file. """ def __init__(self, parent, file): title = _('Rename file') # T: dialog title Dialog.__init__(self, parent, title) assert isinstance(file, LocalFile) and not isinstance(file, LocalFolder) self.old_file = file self.new_file = file # Add field for entering new filename. self.txt_filename = InputEntry() self.txt_filename.set_text(self.old_file.basename) self.txt_filename.set_activates_default( True) # Make ENTER key press trigger the OK button. self.txt_filename.connect("changed", self.do_validate, parent) # Add field for showing hints when an error occurs (e.g. filename already exists). self.txt_error = Gtk.Label() self.txt_error.set_visible(False) # Add ok button. self.btn_ok = self.get_widget_for_response( response_id=Gtk.ResponseType.OK) self.btn_ok.set_can_default(True) self.btn_ok.grab_default() self.btn_ok.set_sensitive(False) # Configure dialog. self.set_modal(True) self.set_default_size(380, 100) self.vbox.pack_start(self.txt_filename, False, True, 0) self.vbox.pack_start(self.txt_error, False, True, 0) # Set focus to search field self.txt_filename.grab_focus() def do_validate(self, widget, data): """ Validating new file name, show error when validation fails and enable/disable ok button. """ def is_filename(filename): """ Returns True when filename does not contain any path declaration. """ return filename == os.path.basename(filename) def does_file_already_exist(filename): """ Checks whether the new filename is already taken. """ return self.old_file.parent().file(filename).exists() def show_error(msg): """ Displays an error and disables the OK button. """ self.txt_error.set_text(msg) self.txt_error.set_visible(True) self.btn_ok.set_sensitive(False) return False if not widget.get_text(): return show_error( _('File name should not be blank.')) # T: Error message if does_file_already_exist(widget.get_text()): return show_error( _('A file with that name already exists.')) # T: Error message if not is_filename(widget.get_text()): return show_error( _('File name should not contain path declaration.') ) # T: Error message # No errors, hide error label and enable OK button. self.txt_error.hide() self.btn_ok.set_sensitive(True) return True def do_response_ok(self): self.new_file = self.old_file.parent().file( self.txt_filename.get_text()) self.result = Gtk.ResponseType.OK self.close()
class SearchDialog(Dialog): READY = 0 SEARCHING = 1 CANCELLED = 2 def __init__(self, ui): Dialog.__init__(self, ui, _('Search'), # T: Dialog title buttons=gtk.BUTTONS_CLOSE, help='Help:Searching', defaultwindowsize=(400, 300) ) hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) hbox.pack_start(gtk.Label(_('Search')+': '), False) # T: input label self.query_entry = InputEntry() hbox.add(self.query_entry) self.search_button = gtk.Button(stock=gtk.STOCK_FIND) hbox.pack_start(self.search_button, False) if gtk.gtk_version >= (2, 20) \ and gtk.pygtk_version >= (2, 22): self.spinner = gtk.Spinner() hbox.pack_start(self.spinner, False) else: self.spinner = None self.cancel_button = gtk.Button(stock=gtk.STOCK_STOP) hbox.pack_start(self.cancel_button, False) self._set_state(self.READY) help_text = _( 'For advanced search you can use operators like\n' 'AND, OR and NOT. See the help page for more details.' ) # T: help text for the search dialog if gtk.gtk_version >= (2, 12, 0): self.query_entry.set_tooltip_text(help_text) else: tooltips = gtk.Tooltips() tooltips.set_tip(self.query_entry, help_text) self.namespacecheckbox = gtk.CheckButton(_('Limit search to current namespace')) # T: checkbox option in search dialog self.vbox.pack_start(self.namespacecheckbox, False) # TODO advanced query editor # TODO checkbox _('Match c_ase') # TODO checkbox _('Whole _word') self.results_treeview = SearchResultsTreeView(self.ui) self.vbox.add(ScrolledWindow(self.results_treeview)) self.search_button.connect_object('clicked', self.__class__._search, self) self.cancel_button.connect_object('clicked', self.__class__._cancel, self) self.query_entry.connect_object('activate', self.__class__._search, self) def search(self, query): '''Trigger a search to be performed. Because search can take a long time to execute it is best to call this method after the dialog is shown. @param query: the query as string ''' self.query_entry.set_text(query) self._search() def _search(self): string = self.query_entry.get_text() if self.namespacecheckbox.get_active(): string = 'Namespace: "%s" ' % self.ui.page.name + string #~ print '!! QUERY: ' + string self._set_state(self.SEARCHING) try: self.results_treeview.search(string) except Exception, error: ErrorDialog(self, error).run() if not self.results_treeview.cancelled: self._set_state(self.READY) else: self._set_state(self.CANCELLED)
def __init__(self, window, index_ext): Dialog.__init__(self, window, _('Task List EDS'), # T: dialog title buttons=gtk.BUTTONS_CLOSE, help=':Plugins:Task List EDS', defaultwindowsize=(550, 400) ) self.index_ext = index_ext hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) self.hpane = HPaned() self.uistate.setdefault('hpane_pos', 75) self.hpane.set_position(self.uistate['hpane_pos']) self.vbox.add(self.hpane) # Task list opener = window.get_resource_opener() self.task_list = TaskListTreeView( self.index_ext, opener ) self.task_list.set_headers_visible(True) # Fix for maemo self.hpane.add2(ScrolledWindow(self.task_list)) # Tag list self.tag_list = TagListTreeView(self.index_ext, self.task_list) self.hpane.add1(ScrolledWindow(self.tag_list)) # Filter input hbox.pack_start(gtk.Label(_('Filter')+': '), False) # T: Input label filter_entry = InputEntry() filter_entry.set_icon_to_clear() hbox.pack_start(filter_entry, False) filter_cb = DelayedCallback(500, lambda o: self.task_list.set_filter(filter_entry.get_text())) filter_entry.connect('changed', filter_cb) # Dropdown with options - TODO #~ menu = gtk.Menu() #~ showtree = gtk.CheckMenuItem(_('Show _Tree')) # T: menu item in options menu #~ menu.append(showtree) #~ menu.append(gtk.SeparatorMenuItem()) #~ showall = gtk.RadioMenuItem(None, _('Show _All Items')) # T: menu item in options menu #~ showopen = gtk.RadioMenuItem(showall, _('Show _Open Items')) # T: menu item in options menu #~ menu.append(showall) #~ menu.append(showopen) #~ menubutton = MenuButton(_('_Options'), menu) # T: Button label #~ hbox.pack_start(menubutton, False) # Statistics label self.statistics_label = gtk.Label() hbox.pack_end(self.statistics_label, False) def set_statistics(): total, stats = self.task_list.get_statistics() text = ngettext('%i open item', '%i open items', total) % total # T: Label for statistics in Task List, %i is the number of tasks text += ' (' + '/'.join(map(str, stats)) + ')' self.statistics_label.set_text(text) set_statistics() def on_tasklist_changed(o): self.task_list.refresh() self.tag_list.refresh(self.task_list) set_statistics() callback = DelayedCallback(10, on_tasklist_changed) # Don't really care about the delay, but want to # make it less blocking - should be async preferably # now it is at least on idle self.connectto(index_ext, 'tasklist-changed', callback)
class SearchDialog(Dialog): READY = 0 SEARCHING = 1 CANCELLED = 2 def __init__(self, widget, notebook, page, navigation): Dialog.__init__( self, widget, _('Search'), # T: Dialog title buttons=Gtk.ButtonsType.CLOSE, help='Help:Searching', defaultwindowsize=(400, 300)) self.page = page hbox = Gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False, True, 0) hbox.pack_start(Gtk.Label(_('Search') + ': '), False, True, 0) # T: input label self.query_entry = InputEntry() hbox.add(self.query_entry) self.search_button = Gtk.Button.new_with_mnemonic( _('_Find')) # T: Button label hbox.pack_start(self.search_button, False, True, 0) self.spinner = Gtk.Spinner() hbox.pack_start(self.spinner, False, True, 0) self.cancel_button = Gtk.Button.new_with_mnemonic( _('_Cancel')) # T: Button label hbox.pack_start(self.cancel_button, False, True, 0) self._set_state(self.READY) help_text = _('For advanced search you can use operators like\n' 'AND, OR and NOT. See the help page for more details.' ) # T: help text for the search dialog self.query_entry.set_tooltip_text(help_text) self.namespacecheckbox = Gtk.CheckButton.new_with_mnemonic( _('Limit search to the current page and sub-pages')) # T: checkbox option in search dialog if page is not None: self.vbox.pack_start(self.namespacecheckbox, False, True, 0) # TODO advanced query editor # TODO checkbox _('Match c_ase') # TODO checkbox _('Whole _word') self.results_treeview = SearchResultsTreeView(notebook, navigation) self.vbox.pack_start(ScrolledWindow(self.results_treeview), True, True, 0) self.search_button.connect_object('clicked', self.__class__._search, self) self.cancel_button.connect_object('clicked', self.__class__._cancel, self) self.query_entry.connect_object('activate', self.__class__._search, self) def search(self, query): '''Trigger a search to be performed. Because search can take a long time to execute it is best to call this method after the dialog is shown. @param query: the query as string ''' self.query_entry.set_text(query) self._search() def _search(self): string = self.query_entry.get_text() if self.namespacecheckbox.get_active(): assert self.page is not None string = 'Section: "%s" ' % self.page.name + string #~ print('!! QUERY: ' + string) self._set_state(self.SEARCHING) try: self.results_treeview.search(string) except Exception as error: ErrorDialog(self, error).run() if not self.results_treeview.cancelled: self._set_state(self.READY) else: self._set_state(self.CANCELLED) def _cancel(self): self.results_treeview.cancelled = True def _set_state(self, state): # TODO set cursor for treeview part # TODO set label or something ? def hide(button): button.hide() button.set_no_show_all(True) def show(button): button.set_no_show_all(False) button.show_all() if state in (self.READY, self.CANCELLED): self.query_entry.set_sensitive(True) hide(self.cancel_button) if self.spinner: self.spinner.stop() hide(self.spinner) show(self.search_button) elif state == self.SEARCHING: self.query_entry.set_sensitive(False) hide(self.search_button) if self.spinner: show(self.spinner) self.spinner.start() show(self.cancel_button) else: assert False, 'BUG: invalid state'
def init_uistate(self): # Initialize all the uistate parameters # delayed till show or show_all because all this needs real # uistate to be in place and plugins to be loaded # Run between loading plugins and actually presenting the window to the user if not self._geometry_set: # Ignore this if an explicit geometry was specified to the constructor if self.uistate['windowpos'] is not None: x, y = self.uistate['windowpos'] self.move(x, y) w, h = self.uistate['windowsize'] self.set_default_size(w, h) if self.uistate['windowmaximized']: self.maximize() # For these two "None" means system default, but we don't know what that default is :( self.preferences.setdefault( 'toolbar_style', None, (TOOLBAR_ICONS_ONLY, TOOLBAR_ICONS_AND_TEXT, TOOLBAR_TEXT_ONLY)) self.preferences.setdefault( 'toolbar_size', None, (TOOLBAR_ICONS_TINY, TOOLBAR_ICONS_SMALL, TOOLBAR_ICONS_LARGE)) self.toggle_toolbar(self.uistate['show_toolbar']) self.toggle_statusbar(self.uistate['show_statusbar']) Window.init_uistate(self) # takes care of sidepane positions etc if self.preferences['toolbar_style'] is not None: self.set_toolbar_style(self.preferences['toolbar_style']) if self.preferences['toolbar_size'] is not None: self.set_toolbar_icon_size(self.preferences['toolbar_size']) self.toggle_fullscreen(self._set_fullscreen) if self.notebook.readonly: self.toggle_editable(False) action = self.actiongroup.get_action('toggle_editable') action.set_sensitive(False) else: self.toggle_editable(not self.uistate['readonly']) # And hook to notebook properties self.on_notebook_properties_changed(self.notebook.properties) self.notebook.properties.connect('changed', self.on_notebook_properties_changed) # Hook up the statusbar self.connect('page-changed', self.do_update_statusbar) self.connect('readonly-changed', self.do_update_statusbar) self.pageview.connect('modified-changed', self.do_update_statusbar) self.notebook.connect_after('stored-page', self.do_update_statusbar) # Notify plugins self.emit('init-uistate') # Update menus etc. self.uimanager.ensure_update() # Prevent flashing when the toolbar is loaded after showing the window # and do this before connecting signal below for accelmap. # Add search bar onec toolbar is loaded space = Gtk.SeparatorToolItem() space.set_draw(False) space.set_expand(True) self.toolbar.insert(space, -1) from zim.gui.widgets import InputEntry entry = InputEntry(placeholder_text=_('Search')) entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_FIND) entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True) entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, _('Search Pages...')) # T: label in search entry inline_search = lambda e, *a: self._uiactions.show_search( query=e.get_text() or None) entry.connect('activate', inline_search) entry.connect('icon-release', inline_search) entry.show() item = Gtk.ToolItem() item.add(entry) self.toolbar.insert(item, -1) # Load accelmap config and setup saving it # TODO - this probably belongs in the application class, not here accelmap = ConfigManager.get_config_file('accelmap').file logger.debug('Accelmap: %s', accelmap.path) if accelmap.exists(): Gtk.AccelMap.load(accelmap.path) def on_accel_map_changed(o, path, key, mod): logger.info('Accelerator changed for %s', path) Gtk.AccelMap.save(accelmap.path) Gtk.AccelMap.get().connect('changed', on_accel_map_changed) self.do_update_statusbar()
class ZoteroDialog(Dialog): """The Zotero specific Input Dialog.""" def __init__(self, pageview, preferences): """Initialize the Input Box with options.""" Dialog.__init__( self, pageview, _('Search in Zotero'), # T: Dialog title button=_('_GO'), # T: Button label defaultwindowsize=(350, 200)) self.pageview = pageview self.textentry = InputEntry() self.vbox.pack_start(self.textentry, False, True, 0) self.preferences = preferences first = None options = [ "Search in Title, Author and Date", "Search in All Fields and Tags", "Search Everywhere" ] for text in options: self.radio = Gtk.RadioButton.new_with_mnemonic_from_widget( first, text) if not first: first = self.radio self.vbox.pack_start(self.radio, False, True, 0) self.radio.show() def run(self): """Call the widget.dialog.run method.""" Dialog.run(self) def do_response_ok(self): """Call to insert citation when pressing ok.""" text = self.textentry.get_text() buffer = self.pageview.textview.get_buffer() active = [r for r in self.radio.get_group() if r.get_active()] # @+ radiotext = active[0].get_label() # @+ self.insert_citation(text, radiotext, buffer) return True def insert_citation(self, text, radiotext, buffer): """Will insert the whole bibliography text.""" root = "127.0.0.1:23119/zotxt" method = '' # Method defaults to titleCreatorYear if "Tags" in radiotext: method = '&method=fields' elif "Everywhere" in radiotext: method = '&method=everything' link_format = self.preferences['link_format'] format = '&format=' + link_format url = 'http://' + root + '/search?q=' + text + format + method try: resp = json.loads(urlopen(url).read()) if link_format == 'bibliography': for i in resp: key = i['key'] try: zotlink = 'zotero://' + root + '/select?key=' + key bibtext = i['text'] buffer.insert_link_at_cursor(bibtext, href=zotlink) buffer.insert_at_cursor("\n") except: pass elif link_format == 'betterbibtexkey': for key in resp: try: zotlink = ('zotero://' + root + '/select?betterbibtexkey=' + key) buffer.insert_link_at_cursor(key, href=zotlink) buffer.insert_at_cursor("\n") except: pass elif link_format == 'easykey': for key in resp: try: zotlink = ('zotero://' + root + '/select?easykey=' + key) buffer.insert_link_at_cursor(key, href=zotlink) buffer.insert_at_cursor("\n") except: pass elif link_format == 'key': for key in resp: try: zotlink = ('zotero://' + root + '/select?key=' + key) buffer.insert_link_at_cursor(key, href=zotlink) buffer.insert_at_cursor("\n") except: pass else: buffer.insert_at_cursor('link format unknown: ' + link_format + "\n") except: pass
def instant_search(self): # init self.cached_titles = [] self.last_query = "" # previous user input self.query_o = None self.caret = SimpleNamespace(pos=0, text="", stick=False) # cursor position self.original_page = self.window.page.name # we return here after escape self.original_history = list(self.window.history.uistate["list"]) self.selection = None if not self.plugin.preferences['is_cached']: # reset last search results State.reset() self.menu_page = None self.is_closed = False self.last_page = None # building quick title cache def build(start=""): o = self.window.notebook.pages for s in o.list_pages(Path(start or ":")): start2 = (start + ":" if start else "") + s.basename self.cached_titles.append((start2, start2.lower())) build(start2) build() # Gtk self.gui = Dialog(self.window, _('Search'), buttons=None, defaultwindowsize=(300, -1)) self.gui.resize(300, 100) # reset size self.input_entry = InputEntry() self.input_entry.connect('key_press_event', self.move) self.input_entry.connect( 'changed', self.change) # self.change is needed by GObject or something self.gui.vbox.pack_start(self.input_entry, expand=False, fill=True, padding=0) # noinspection PyArgumentList self.label_object = Gtk.Label(label='') self.label_object.set_size_request(300, -1) self.gui.vbox.pack_start(self.label_object, expand=False, fill=True, padding=0) # preview pane self.label_preview = Gtk.Label(label='...loading...') # not sure if this has effect, longer lines without spaces still make window inflate self.label_preview.set_line_wrap(True) self.label_preview.set_xalign(0) # align to the left self.preview_pane = Gtk.VBox() self.preview_pane.pack_start(self.label_preview, False, False, 5) self.window.pageview.pack_start(self.preview_pane, False, False, 5) # gui geometry self.geometry(init=True) self.gui.show_all()
class InsertSymbolDialog(Dialog): def __init__(self, parent, plugin, pageview): Dialog.__init__( self, parent, _('Insert Symbol'), # T: Dialog title button=_('_Insert'), # T: Button label defaultwindowsize=(350, 400)) self.plugin = plugin self.pageview = pageview if not plugin.symbols: plugin.load_file() self.textentry = InputEntry() self.vbox.pack_start(self.textentry, False, True, 0) model = Gtk.ListStore(str, str) # text, shortcut self.iconview = Gtk.IconView(model) self.iconview.set_text_column(0) self.iconview.set_column_spacing(0) self.iconview.set_row_spacing(0) self.iconview.set_property('has-tooltip', True) self.iconview.set_property('activate-on-single-click', True) self.iconview.connect('query-tooltip', self.on_query_tooltip) self.iconview.connect('item-activated', self.on_activated) swindow = ScrolledWindow(self.iconview) self.vbox.pack_start(swindow, True, True, 0) button = Gtk.Button.new_with_mnemonic(_('_Edit')) # T: Button label button.connect('clicked', self.on_edit) self.action_area.add(button) self.action_area.reorder_child(button, 0) self.load_symbols() def load_symbols(self): model = self.iconview.get_model() model.clear() for symbol, shortcut in self.plugin.get_symbols(): model.append((symbol, shortcut)) def on_query_tooltip(self, iconview, x, y, keyboard, tooltip): if keyboard: return False x, y = iconview.convert_widget_to_bin_window_coords(x, y) path = iconview.get_path_at_pos(x, y) if path is None: return False model = iconview.get_model() iter = model.get_iter(path) text = model.get_value(iter, 1) if not text: return False tooltip.set_text(text) return True def on_activated(self, iconview, path): model = iconview.get_model() iter = model.get_iter(path) text = model.get_value(iter, 0) pos = self.textentry.get_position() self.textentry.insert_text(text, pos) self.textentry.set_position(pos + len(text)) def on_edit(self, button): file = self.plugin.config.get_config_file('symbols.list') if edit_config_file(self, file): self.plugin.load_file() self.load_symbols() def run(self): self.iconview.grab_focus() Dialog.run(self) def do_response_ok(self): text = self.textentry.get_text() textview = self.pageview.textview buffer = textview.get_buffer() buffer.insert_at_cursor(text) return True
class InsertSymbolDialog(Dialog): def __init__(self, ui, plugin): Dialog.__init__(self, ui, _('Insert Symbol'), # T: Dialog title button=(_('_Insert'), 'gtk-ok'), # T: Button label defaultwindowsize=(350, 400) ) self.plugin = plugin self.textentry = InputEntry() self.vbox.pack_start(self.textentry, False) # TODO make this iconview single-click model = gtk.ListStore(str, str) # text, shortcut self.iconview = gtk.IconView(model) self.iconview.set_text_column(0) self.iconview.set_column_spacing(0) self.iconview.set_row_spacing(0) if gtk.gtk_version >= (2, 12, 0): self.iconview.set_property('has-tooltip', True) self.iconview.connect('query-tooltip', self.on_query_tooltip) self.iconview.connect('item-activated', self.on_activated) self.vbox.add(ScrolledWindow(self.iconview)) button = gtk.Button(stock=gtk.STOCK_EDIT) button.connect('clicked', self.on_edit) self.action_area.add(button) self.action_area.reorder_child(button, 0) self.load_symbols() def load_symbols(self): model = self.iconview.get_model() model.clear() for symbol, shortcut in self.plugin.get_symbols(): model.append((symbol, shortcut)) def on_query_tooltip(self, iconview, x, y, keyboard, tooltip): if keyboard: return False x, y = iconview.convert_widget_to_bin_window_coords(x, y) path = iconview.get_path_at_pos(x, y) if path is None: return False model = iconview.get_model() iter = model.get_iter(path) text = model.get_value(iter, 1) if not text: return False tooltip.set_text(text) return True def on_activated(self, iconview, path): model = iconview.get_model() iter = model.get_iter(path) text = model.get_value(iter, 0) text = text.decode('utf-8') pos = self.textentry.get_position() self.textentry.insert_text(text, pos) self.textentry.set_position(pos + len(text)) def on_edit(self, button): file = config_file('symbols.list') if self.ui.edit_config_file(file): self.plugin.load_file() self.load_symbols() def run(self): self.iconview.grab_focus() Dialog.run(self) def do_response_ok(self): text = self.textentry.get_text() textview = self.plugin.pageview.view buffer = textview.get_buffer() buffer.insert_at_cursor(text) return True
class InstantsearchMainWindowExtension(WindowExtension): uimanager_xml = ''' <ui> <menubar name='menubar'> <menu action='tools_menu'> <placeholder name='plugin_items'> <menuitem action='instantsearch'/> </placeholder> </menu> </menubar> </ui> ''' gui = ""; @action(_('_Instantsearch'), accelerator='<ctrl>e') # T: menu item def instantsearch(self): #init self.cached_titles = [] #self.menu = defaultdict(_MenuItem) self.lastQuery = "" # previous user input self.queryO = None self.caret = {'pos':0, 'altPos':0, 'text':""} # cursor position self.originalPage = self.window.ui.page.name # we return here after escape self.selection = None if not self.plugin.preferences['isCached']: # reset last search results State.reset() self.menuPage = None self.isClosed = False self.lastPage = None # preferences self.title_match_char = self.plugin.preferences['title_match_char'] self.start_search_length = self.plugin.preferences['start_search_length'] self.keystroke_delay = self.plugin.preferences['keystroke_delay'] self.open_when_unique = self.plugin.preferences['open_when_unique'] # building quick title cache def build(start = ""): if hasattr(self.window.ui.notebook, 'pages'): o = self.window.ui.notebook.pages else: # for Zim 0.66- o = self.window.ui.notebook.index for s in o.list_pages(Path(start or ":")): start2 = (start + ":" if start else "") + s.basename self.cached_titles.append((start2, start2.lower())) build(start2) build() # Gtk self.gui = Dialog(self.window.ui, _('Search'), buttons=None, defaultwindowsize=(300, -1)) self.gui.resize(300, 100) # reset size self.inputEntry = InputEntry() self.inputEntry.connect('key_press_event', self.move) self.inputEntry.connect('changed', self.change) # self.change is needed by GObject or something self.gui.vbox.pack_start(self.inputEntry, False) self.labelObject = gtk.Label(('')) self.labelObject.set_usize(300, -1) self.gui.vbox.pack_start(self.labelObject, False) #gui geometry px, py = self.window.get_position() pw, ph = self.window.get_size() x, y = self.gui.get_position() if self.plugin.preferences['position'] == InstantsearchPlugin.POSITION_RIGHT: self.gui.move((pw-300), 0) elif self.plugin.preferences['position'] == InstantsearchPlugin.POSITION_CENTER: self.gui.resize(300, 100) self.gui.move(px + (pw / 2) - 150, py + (ph / 2) - 250) else: raise AttributeError("Instant search: Wrong position preference.") self.gui.show_all() self.labelVar = "" self.timeout = "" self.timeoutOpenPage = None #lastPage = "" #pageTitleOnly = False menu = [] #queryTime = 0 def change(self, _): #widget, event,text if self.timeout: gobject.source_remove(self.timeout) q = self.inputEntry.get_text() #print("Change. {} {}".format(input, self.lastQuery)) if q == self.lastQuery: return if q == self.title_match_char: return if q and q[-1] == "∀": # easter egg: debug option for zim --standalone q = q[:-1] import ipdb; ipdb.set_trace() self.state = State.setCurrent(q) if not self.state.isFinished: self.isSubset = True if self.lastQuery and q.startswith(self.lastQuery) else False self.state.checkTitleSearch(self.title_match_char) self.startSearch() else: # search completed before #print("Search already cached.") self.startSearch() # update the results in a page has been modified meanwhile (not if something got deleted in the notebook #16 ) self.checkLast() self.soutMenu() self.lastQuery = q def startSearch(self): """ Search string has certainly changed. We search in indexed titles and/or we start zim search. Normally, zim gives 11 points bonus if the search-string appears in the titles. If we are ignoring subpages, the search "foo" will match only page "journal:foo", but not "journal:foo:subpage" (and score of the parent page will get slightly higher by 1.) However, if there are occurences of the string in the fulltext of the subpage, subpage remains in the result, but gets bonus only 2 points (not 11). """ query = self.state.query menu = self.state.menu isInQuery = re.compile(r"(^|:|\s|\()" + query).search # 'te' matches this page titles: 'test' or 'Journal:test' or 'foo test' or 'foo (test)' if self.isSubset and len(query) < self.start_search_length: # letter(s) was/were added and full search has not yet been activated for path in _MenuItem.titles: if path in self.state.menu and not isInQuery(path.lower()): # 'te' didnt match 'test' etc del menu[path] # we pop out the result else: menu[path].sure = True else: # perform new search in cached_titles _MenuItem.titles = set() found = 0 if self.state.firstSeen: for path, pathLow in self.cached_titles: # quick search in titles if isInQuery(pathLow): # 'te' matches 'test' or 'Journal:test' etc _MenuItem.titles.add(path) if query in path.lower() and query not in path.lower().split(":")[-1]: # "raz" in "raz:dva", but not in "dva" self.state.menu[":".join(path.split(":")[:-1])].bonus += 1 # 1 point for subpage menu[path].bonus = -11 menu[path].score += 10 # 10 points for title (zim default) (so that it gets displayed before search finishes) menu[path].intitle = True menu[path].path = path found += 1 if found >= 10: # we dont want more than 10 results; we would easily match all of the pages break else: menu[path].intitle = False self.processMenu() # show for now results of title search if len(query) >= self.start_search_length: self.timeout = gobject.timeout_add(self.keystroke_delay, self.startZimSearch) # ideal delay between keystrokes def startZimSearch(self): """ Starts search for the input. """ self.timeout = "" self.caret['altPos'] = 0 # possible position of caret - beginning s = '"*{}*"'.format(self.state.query) if self.plugin.preferences['isWildcarded'] else self.state.query self.queryO = Query(unicode(s)) # beware when searching for unicode character. Update the row when going to Python3. lastSel = self.selection if self.isSubset and self.state.previous.isFinished else None # it should be quicker to find the string, if we provide this subset from last time (in the case we just added a letter, so that the subset gets smaller) self.selection = SearchSelection(self.window.ui.notebook) state = self.state # this is thread, so that self.state would can before search finishes self.selection.search(self.queryO, selection=lastSel, callback=self._search_callback(self.state.rawQuery)) state.isFinished = True for item in list(state.menu): # remove all the items that we didnt encounter during the search if not state.menu[item].sure: del state.menu[item] if state == self.state: self.checkLast() self.processMenu(state=state) def checkLast(self): """ opens the page if there is only one option in the menu """ if self.open_when_unique and len(self.state.menu) == 1: self._open_page(Path(self.state.menu.keys()[0]), excludeFromHistory=False) self.close() def _search_callback(self, query): def _search_callback(results, path): if results is not None: self._update_results(results, State.get(query)) # we finish the search even if another search is running. If we returned False, the search would be cancelled- while gtk.events_pending(): gtk.main_iteration(block=False) return True return _search_callback def _update_results(self, results, state): """ This method may run many times, due to the _update_results, which are updated many times. I may set that _update_results would run only once, but this is nice - the results are appearing one by one. """ changed = False state.lastResults = results for option in results.scores: if state.pageTitleOnly and state.query not in option.name: # hledame jen v nazvu stranky continue if option.name not in state.menu: # new item found if state == self.state and option.name == self.caret['text']: # this is current search self.caret['altPos'] = len(state.menu)-1 #karet byl na tehle pozici, pokud se zuzil vyber, budeme vedet, kam karet opravne umistit if option.name not in state.menu or (state.menu[option.name].bonus < 0 and state.menu[option.name].score == 0): changed = True if not state.menu[option.name].sure: state.menu[option.name].sure = True changed = True state.menu[option.name].score = results.scores[option] #zaradit mezi moznosti if changed: # we added a page self.processMenu(state=state, sort=False) else: pass def processMenu(self, state=None, sort=True): """ Sort menu and generate items and sout menu. """ if state is None: state = self.state if sort: state.items = sorted(state.menu, reverse=True, key=lambda item: (state.menu[item].intitle, state.menu[item].score + state.menu[item].bonus, -item.count(":"), item)) else: # when search results are being updated, it's good when the order doesnt change all the time. So that the first result does not become for a while 10th and then become first back. state.items = sorted(state.menu, reverse=True, key=lambda item: (state.menu[item].intitle, -state.menu[item].lastOrder)) state.items = [item for item in state.items if (state.menu[item].score + state.menu[item].bonus) > 0] # i dont know why there are items with 0 score if state == self.state: self.soutMenu() def soutMenu(self, displayImmediately=False): """ Displays menu and handles caret position. """ if self.timeoutOpenPage: gobject.source_remove(self.timeoutOpenPage) self.gui.resize(300, 100) # reset size # treat possible caret deflection if self.caret['pos'] < 0 or self.caret['pos'] > len(self.state.items)-1: # place the caret to the beginning or the end of list self.caret['pos'] = self.caret['altPos'] text = "" i = 0 for item in self.state.items: score = self.state.menu[item].score + self.state.menu[item].bonus self.state.menu[item].lastOrder = i if i == self.caret['pos']: self.caret['text'] = item # caret is at this position text += '→ {} ({}) {}\n'.format(item, score, "" if self.state.menu[item].sure else "?") else: try: text += '{} ({}) {}\n'.format(item, score, "" if self.state.menu[item].sure else "?") except: text += "CHYBA\n" text += item[0:-1] + "\n" i += 1 self.labelObject.set_text(text) self.menuPage = Path(self.caret['text'] if len(self.state.items) else self.originalPage) if not displayImmediately: self.timeoutOpenPage = gobject.timeout_add(self.keystroke_delay, self._open_page, self.menuPage) # ideal delay between keystrokes else: self._open_page(self.menuPage) def move(self, widget, event): """ Move caret up and down. Enter to confirm, Esc closes search.""" keyname = gtk.gdk.keyval_name(event.keyval) if keyname == "Up" or keyname == "ISO_Left_Tab": self.caret['pos'] -= 1 self.soutMenu(displayImmediately=False) if keyname == "Down" or keyname == "Tab": self.caret['pos'] += 1 self.soutMenu(displayImmediately=False) if keyname == "KP_Enter" or keyname == "Return": self._open_page(self.menuPage, excludeFromHistory=False) self.close() if keyname == "Escape": self._open_original() # GTK closes the windows itself on Escape, no self.close() needed return ## Safely closes # Xwhen closing directly, Python gave allocation error def close(self): if not self.isClosed: self.isClosed = True self.gui.emit("close") def _open_original(self): self._open_page(Path(self.originalPage)) def _open_page(self, page, excludeFromHistory=True): """ Open page and highlight matches """ self.timeoutOpenPage = None # no delayed page will be open if self.isClosed == True: return if page and page.name and page.name != self.lastPage: self.lastPage = page.name #print("*** HISTORY BEF", self.window.ui.history._history[-3:]) self.window.ui.open_page(page) if excludeFromHistory: # there is no public API, so lets use protected _history instead self.window.ui.history._history.pop() self.window.ui.history._current = len(self.window.ui.history._history) - 1 if not excludeFromHistory and self.window.ui.history.get_current().name is not page.name: # we insert the page to the history because it was likely to be just visited and excluded self.window.ui.history.append(page) # Popup find dialog with same query if self.queryO:# and self.queryO.simple_match: string = self.state.query string = string.strip('*') # support partial matches if self.plugin.preferences['highlight_search']: self._get_mainwindow().pageview.show_find(string, highlight=True) def _get_mainwindow(self): # #18 try: return self.window.ui._mainwindow except AttributeError: return self.window.ui.mainwindow
def __init__(self, parent, tasksview, properties): Dialog.__init__( self, parent, _('Task List'), # T: dialog title buttons=Gtk.ButtonsType.CLOSE, help=':Plugins:Task List', defaultwindowsize=(550, 400)) self.properties = properties self.tasksview = tasksview self.notebook = parent.notebook hbox = Gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False, True, 0) self.hpane = HPaned() self.uistate.setdefault('hpane_pos', 75) self.hpane.set_position(self.uistate['hpane_pos']) self.vbox.pack_start(self.hpane, True, True, 0) # Task list self.uistate.setdefault('only_show_act', False) self.uistate.setdefault('show_flatlist', False) self.uistate.setdefault('sort_column', 0) self.uistate.setdefault('sort_order', int(Gtk.SortType.DESCENDING)) opener = parent.navigation self.task_list = TaskListTreeView( self.tasksview, opener, _parse_task_labels(properties['labels']), nonactionable_tags=_parse_task_labels( properties['nonactionable_tags']), filter_actionable=self.uistate['only_show_act'], tag_by_page=properties['tag_by_page'], use_workweek=properties['use_workweek'], flatlist=self.uistate['show_flatlist'], sort_column=self.uistate['sort_column'], sort_order=self.uistate['sort_order']) self.task_list.set_headers_visible(True) self.task_list.connect('populate-popup', self.on_populate_popup) self.hpane.add2(ScrolledWindow(self.task_list)) # Tag list self.tag_list = TagListTreeView(self.task_list) self.hpane.add1(ScrolledWindow(self.tag_list)) self.connectto(properties, 'changed', self.on_properties_changed) # Filter input hbox.pack_start(Gtk.Label(_('Filter') + ': '), False, True, 0) # T: Input label filter_entry = InputEntry() filter_entry.set_icon_to_clear() hbox.pack_start(filter_entry, False, True, 0) filter_cb = DelayedCallback( 500, lambda o: self.task_list.set_filter(filter_entry.get_text())) filter_entry.connect('changed', filter_cb) # TODO: use menu button here and add same options as in context menu # for filtering the list def on_show_active_toggle(o): active = self.act_toggle.get_active() if self.uistate['only_show_act'] != active: self.uistate['only_show_act'] = active self.task_list.set_filter_actionable(active) self.act_toggle = Gtk.CheckButton.new_with_mnemonic( _('Only Show Active Tasks')) # T: Checkbox in task list - this options hides tasks that are not yet started self.act_toggle.set_active(self.uistate['only_show_act']) self.act_toggle.connect('toggled', on_show_active_toggle) self.uistate.connect( 'changed', lambda o: self.act_toggle.set_active(self.uistate[ 'only_show_act'])) hbox.pack_start(self.act_toggle, False, True, 0) # Statistics label self.statistics_label = Gtk.Label() hbox.pack_end(self.statistics_label, False, True, 0) def set_statistics(): total = self.task_list.get_n_tasks() text = ngettext('%i open item', '%i open items', total) % total # T: Label for task List, %i is the number of tasks self.statistics_label.set_text(text) set_statistics() def on_tasklist_changed(o): self.task_list.refresh() self.tag_list.refresh(self.task_list) set_statistics() callback = DelayedCallback(10, on_tasklist_changed) # Don't really care about the delay, but want to # make it less blocking - should be async preferably # now it is at least on idle from . import TaskListNotebookExtension nb_ext = find_extension(self.notebook, TaskListNotebookExtension) self.connectto(nb_ext, 'tasklist-changed', callback)
class GoogletasksNewTaskDialog(Dialog): def __init__(self, *args, task=None, controller=None, **kwargs): self.input_title = None self.input_due = None self.input_notes = None self.label_object = None self.label_due = None self.task = task self.controller = controller super().__init__(*args, **kwargs) # noinspection PyArgumentList def setup(self): self.resize(300, 100) # reset size # title self.label_object = Gtk.Label(label='Update Google task' if self.task.get("id", "") else 'Create Google tasks') self.label_object.set_size_request(300, -1) self.vbox.pack_start(self.label_object, expand=False, fill=True, padding=0) # editable text fields (title and notes) self.input_title = InputEntry(allow_empty=False, placeholder_text="task title") self.vbox.pack_start(self.input_title, expand=False, fill=True, padding=0) self.input_notes = Gtk.TextView() # date field self.input_due = InputEntry(allow_empty=False) try: s = self.controller.get_time(mode="date-only", from_string=self.task["due"], past_dates=False) except (ValueError, KeyError): # task["due"] is not set or is in past s = self.controller.get_time(add_days=1, mode="date-only") self.input_due.set_text(s) self.input_due.connect('changed', self.update_date) self.label_due = Gtk.Label(self.controller.get_time(add_days=1, mode="day")) hbox = Gtk.HBox(spacing=1) hbox.pack_start(self.input_due, expand=True, fill=True, padding=0) hbox.pack_start(self.label_due, expand=True, fill=True, padding=0) self.vbox.pack_start(hbox, expand=False, fill=True, padding=0) # we cant tab out from notes textarea field, hence its placed under date self.vbox.pack_start(self.input_notes, expand=False, fill=True, padding=0) # arbitrary postponing buttons hbox = Gtk.HBox() def butt(label=None, date=None, days=None): """ Call either with label and date are with days.""" if days: label = f"{'_' if days < 10 else ''}{days} {self.controller.get_time(add_days=days, mode='day')}" date = self.controller.get_time(add_days=days, mode="date-only") b = Gtk.Button.new_with_mnemonic(label=label) b.connect("clicked", self.postpone(date)) b.set_tooltip_text(date) hbox.pack_start(b, expand=False, fill=True, padding=0) if self.controller.preferences["button_monday"]: butt("_Monday", self._slippy_date(next_monday=True)) if self.controller.preferences["button_next_monday"]: butt("_Next Monday", self._slippy_date(next_monday=True, relative_delta={"days": 7})) if self.controller.preferences["button_next_month"]: butt("Mon_th", self._slippy_date(relative_delta={"months": 1, "day": 1})) if self.controller.preferences["button_next_year"]: butt("_Year", self._slippy_date(relative_delta={"years": 1, "month": 2, "day": 1})) if hbox.get_children(): # there were some buttons to display self.vbox.add(hbox) # 9 (or more) postponing buttons days = self.controller.preferences["postponing_days"] if days: hbox = Gtk.HBox() self.vbox.add(hbox) hbox.pack_start(Gtk.Label(label='Days: '), expand=False, fill=True, padding=0) for i in range(1, days + 1): butt(days=i) if not i % 9: # put every 9 buttons to another line, do not expand the row to infinity hbox = Gtk.HBox() self.vbox.add(hbox) # predefined fields if "title" in self.task: self.input_title.set_text(self.task["title"]) if "notes" in self.task: self.input_notes.get_buffer().set_text(self.task["notes"]) if "due" in self.task: logger.error("Not yet implemented") # XX # display window self.show_all() return self def _slippy_date(self, add_days=0, next_monday=False, relative_delta=None): def next_weekday(d, weekday): days_ahead = weekday - d.weekday() if days_ahead <= 0: # Target day already happened this week days_ahead += 7 return d + datetime.timedelta(days_ahead) d = self.controller.get_time(add_days=add_days, mode="object") if next_monday: d = next_weekday(d, 0) # 0 = Monday, 1=Tuesday, 2=Wednesday... if relative_delta: d += relativedelta(**relative_delta) return self.controller.get_time(use_date=d, mode="date-only") def update_date(self, _): try: day = self.controller.get_time(from_string=self.input_due.get_text(), mode="day", past_dates=False) except ValueError: day = INVALID_DAY self.label_due.set_text(day) def _load_task(self): # noinspection PyBroadException try: o = self.input_notes.get_buffer() self.task["notes"] = o.get_text(*o.get_bounds(), include_hidden_chars=True) self.task["title"] = self.input_title.get_text() if self.input_due.get_text(): try: self.task["due"] = self.controller.get_time(from_string=self.input_due.get_text(), mode="morning") except Exception: logger.error("Failed due date parsing") raise except Exception: pass def do_response(self, id_): """ we cant use parent function because Esc is not handled as cancel """ if id_ == Gtk.ResponseType.OK: self.do_response_ok() else: self.do_response_cancel() def do_response_ok(self): if self.label_due.get_text() == INVALID_DAY: return False self._load_task() self.destroy() # immediately close (so that we wont hit Ok twice) if not self.controller.submit_task(task=self.task): self.do_response_cancel() return True def do_response_cancel(self): """ something failed, restore original text in the zim-page """ text = self.controller.get_task_text(self.task, self.controller.preferences["include_start_date"]) if text: buffer = self.controller.window.pageview.textview.get_buffer() buffer.insert_parsetree_at_cursor(Parser().parse(text)) self.destroy() # def postpone(self, _, number): # self.input_due.set_text(self.controller.get_time(add_days=number, mode="date-only")) # self.do_response_ok() def postpone(self, date): def _(_): self.input_due.set_text(date) self.do_response_ok() return _
class SearchDialog(Dialog): READY = 0 SEARCHING = 1 CANCELLED = 2 def __init__(self, ui): Dialog.__init__( self, ui, _('Search'), # T: Dialog title buttons=gtk.BUTTONS_CLOSE, help='Help:Searching', defaultwindowsize=(400, 300)) hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) hbox.pack_start(gtk.Label(_('Search') + ': '), False) # T: input label self.query_entry = InputEntry() hbox.add(self.query_entry) self.search_button = gtk.Button(stock=gtk.STOCK_FIND) hbox.pack_start(self.search_button, False) if gtk.gtk_version >= (2, 20) \ and gtk.pygtk_version >= (2, 22): # update in pygtk was later self.spinner = gtk.Spinner() hbox.pack_start(self.spinner, False) else: self.spinner = None self.cancel_button = gtk.Button(stock=gtk.STOCK_STOP) hbox.pack_start(self.cancel_button, False) self._set_state(self.READY) help_text = _('For advanced search you can use operators like\n' 'AND, OR and NOT. See the help page for more details.' ) # T: help text for the search dialog if gtk.gtk_version >= (2, 12) \ and gtk.pygtk_version >= (2, 12): self.query_entry.set_tooltip_text(help_text) else: tooltips = gtk.Tooltips() tooltips.set_tip(self.query_entry, help_text) self.namespacecheckbox = gtk.CheckButton( _('Limit search to the current page and sub-pages')) # T: checkbox option in search dialog self.vbox.pack_start(self.namespacecheckbox, False) # TODO advanced query editor # TODO checkbox _('Match c_ase') # TODO checkbox _('Whole _word') self.results_treeview = SearchResultsTreeView(self.ui) self.vbox.add(ScrolledWindow(self.results_treeview)) self.search_button.connect_object('clicked', self.__class__._search, self) self.cancel_button.connect_object('clicked', self.__class__._cancel, self) self.query_entry.connect_object('activate', self.__class__._search, self) def search(self, query): '''Trigger a search to be performed. Because search can take a long time to execute it is best to call this method after the dialog is shown. @param query: the query as string ''' self.query_entry.set_text(query) self._search() def _search(self): string = self.query_entry.get_text() if self.namespacecheckbox.get_active(): string = 'Section: "%s" ' % self.ui.page.name + string #~ print '!! QUERY: ' + string self._set_state(self.SEARCHING) try: self.results_treeview.search(string) except Exception, error: ErrorDialog(self, error).run() if not self.results_treeview.cancelled: self._set_state(self.READY) else: self._set_state(self.CANCELLED)
def setup(self): self.resize(300, 100) # reset size # title self.label_object = Gtk.Label(label='Update Google task' if self.task.get("id", "") else 'Create Google tasks') self.label_object.set_size_request(300, -1) self.vbox.pack_start(self.label_object, expand=False, fill=True, padding=0) # editable text fields (title and notes) self.input_title = InputEntry(allow_empty=False, placeholder_text="task title") self.vbox.pack_start(self.input_title, expand=False, fill=True, padding=0) self.input_notes = Gtk.TextView() # date field self.input_due = InputEntry(allow_empty=False) try: s = self.controller.get_time(mode="date-only", from_string=self.task["due"], past_dates=False) except (ValueError, KeyError): # task["due"] is not set or is in past s = self.controller.get_time(add_days=1, mode="date-only") self.input_due.set_text(s) self.input_due.connect('changed', self.update_date) self.label_due = Gtk.Label(self.controller.get_time(add_days=1, mode="day")) hbox = Gtk.HBox(spacing=1) hbox.pack_start(self.input_due, expand=True, fill=True, padding=0) hbox.pack_start(self.label_due, expand=True, fill=True, padding=0) self.vbox.pack_start(hbox, expand=False, fill=True, padding=0) # we cant tab out from notes textarea field, hence its placed under date self.vbox.pack_start(self.input_notes, expand=False, fill=True, padding=0) # arbitrary postponing buttons hbox = Gtk.HBox() def butt(label=None, date=None, days=None): """ Call either with label and date are with days.""" if days: label = f"{'_' if days < 10 else ''}{days} {self.controller.get_time(add_days=days, mode='day')}" date = self.controller.get_time(add_days=days, mode="date-only") b = Gtk.Button.new_with_mnemonic(label=label) b.connect("clicked", self.postpone(date)) b.set_tooltip_text(date) hbox.pack_start(b, expand=False, fill=True, padding=0) if self.controller.preferences["button_monday"]: butt("_Monday", self._slippy_date(next_monday=True)) if self.controller.preferences["button_next_monday"]: butt("_Next Monday", self._slippy_date(next_monday=True, relative_delta={"days": 7})) if self.controller.preferences["button_next_month"]: butt("Mon_th", self._slippy_date(relative_delta={"months": 1, "day": 1})) if self.controller.preferences["button_next_year"]: butt("_Year", self._slippy_date(relative_delta={"years": 1, "month": 2, "day": 1})) if hbox.get_children(): # there were some buttons to display self.vbox.add(hbox) # 9 (or more) postponing buttons days = self.controller.preferences["postponing_days"] if days: hbox = Gtk.HBox() self.vbox.add(hbox) hbox.pack_start(Gtk.Label(label='Days: '), expand=False, fill=True, padding=0) for i in range(1, days + 1): butt(days=i) if not i % 9: # put every 9 buttons to another line, do not expand the row to infinity hbox = Gtk.HBox() self.vbox.add(hbox) # predefined fields if "title" in self.task: self.input_title.set_text(self.task["title"]) if "notes" in self.task: self.input_notes.get_buffer().set_text(self.task["notes"]) if "due" in self.task: logger.error("Not yet implemented") # XX # display window self.show_all() return self
def __init__(self, plugin): if ui_environment['platform'] == 'maemo': defaultsize = (800, 480) else: defaultsize = (550, 400) Dialog.__init__(self, plugin.ui, _('Task List'), # T: dialog title buttons=gtk.BUTTONS_CLOSE, help=':Plugins:Task List', defaultwindowsize=defaultsize) self.plugin = plugin if ui_environment['platform'] == 'maemo': self.resize(800, 480) # Force maximum dialog size under maemo, otherwise # we'll end with a too small dialog and no way to resize it hbox = gtk.HBox(spacing=5) self.vbox.pack_start(hbox, False) self.hpane = HPaned() self.uistate.setdefault('hpane_pos', 75) self.hpane.set_position(self.uistate['hpane_pos']) self.vbox.add(self.hpane) # Task list self.uistate.setdefault('only_show_act', False) self.task_list = TaskListTreeView(self.ui, plugin, filter_actionable=self.uistate['only_show_act']) self.task_list.set_headers_visible(True) # Fix for maemo self.hpane.add2(ScrolledWindow(self.task_list)) # Tag list self.tag_list = TagListTreeView(self.task_list) self.hpane.add1(ScrolledWindow(self.tag_list)) # Filter input hbox.pack_start(gtk.Label(_('Filter') + ': '), False) # T: Input label filter_entry = InputEntry() filter_entry.set_icon_to_clear() hbox.pack_start(filter_entry, False) filter_cb = DelayedCallback(500, lambda o: self.task_list.set_filter(filter_entry.get_text())) filter_entry.connect('changed', filter_cb) # Dropdown with options - TODO # ~ menu = gtk.Menu() # ~ showtree = gtk.CheckMenuItem(_('Show _Tree')) # T: menu item in options menu # ~ menu.append(showtree) # ~ menu.append(gtk.SeparatorMenuItem()) # ~ showall = gtk.RadioMenuItem(None, _('Show _All Items')) # T: menu item in options menu # ~ showopen = gtk.RadioMenuItem(showall, _('Show _Open Items')) # T: menu item in options menu # ~ menu.append(showall) # ~ menu.append(showopen) # ~ menubutton = MenuButton(_('_Options'), menu) # T: Button label # ~ hbox.pack_start(menubutton, False) self.act_toggle = gtk.CheckButton(_('Only Show Actionable Tasks')) # T: Checkbox in task list self.act_toggle.set_active(self.uistate['only_show_act']) self.act_toggle.connect('toggled', lambda o: self.task_list.set_filter_actionable(o.get_active())) hbox.pack_start(self.act_toggle, False) # Statistics label self.statistics_label = gtk.Label() hbox.pack_end(self.statistics_label, False) def set_statistics(): total, stats = self.task_list.get_statistics() text = ngettext('%i open item', '%i open items', total) % total # T: Label for statistics in Task List, %i is the number of tasks text += ' (' + '/'.join(map(str, stats)) + ')' self.statistics_label.set_text(text) set_statistics() def on_tasklist_changed(o): self.task_list.refresh() self.tag_list.refresh(self.task_list) set_statistics() callback = DelayedCallback(10, on_tasklist_changed) # Don't really care about the delay, but want to # make it less blocking - should be async preferably # now it is at least on idle self.connectto(plugin, 'tasklist-changed', callback)
class InstantsearchMainWindowExtension(MainWindowExtension): uimanager_xml = ''' <ui> <menubar name='menubar'> <menu action='tools_menu'> <placeholder name='plugin_items'> <menuitem action='instantsearch'/> </placeholder> </menu> </menubar> </ui> ''' gui = "" @action(_('_Instantsearch'), accelerator='<ctrl>e') # T: menu item def instantsearch(self): # init self.cached_titles = [] # self.menu = defaultdict(_MenuItem) self.lastQuery = "" # previous user input self.queryO = None self.caret = {'pos': 0, 'altPos': 0, 'text': ""} # cursor position self.originalPage = self.window.page.name # we return here after escape self.selection = None if not self.plugin.preferences['isCached']: # reset last search results State.reset() self.menuPage = None self.isClosed = False self.lastPage = None # preferences self.title_match_char = self.plugin.preferences['title_match_char'] self.start_search_length = self.plugin.preferences[ 'start_search_length'] self.keystroke_delay = self.plugin.preferences['keystroke_delay'] self.open_when_unique = self.plugin.preferences['open_when_unique'] # building quick title cache def build(start=""): if hasattr(self.window.notebook, 'pages'): o = self.window.notebook.pages else: # for Zim 0.66- o = self.window.notebook.index for s in o.list_pages(Path(start or ":")): start2 = (start + ":" if start else "") + s.basename self.cached_titles.append((start2, start2.lower())) build(start2) build() # Gtk self.gui = Dialog(self.window, _('Search'), buttons=None, defaultwindowsize=(300, -1)) self.gui.resize(300, 100) # reset size self.inputEntry = InputEntry() self.inputEntry.connect('key_press_event', self.move) self.inputEntry.connect( 'changed', self.change) # self.change is needed by GObject or something self.gui.vbox.pack_start(self.inputEntry, expand=False, fill=True, padding=0) self.labelObject = Gtk.Label(label=('')) self.labelObject.set_size_request(300, -1) self.gui.vbox.pack_start(self.labelObject, expand=False, fill=True, padding=0) # gui geometry px, py = self.window.get_position() pw, ph = self.window.get_size() x, y = self.gui.get_position() if self.plugin.preferences[ 'position'] == InstantsearchPlugin.POSITION_RIGHT: self.gui.move((pw - 300), 0) elif self.plugin.preferences[ 'position'] == InstantsearchPlugin.POSITION_CENTER: self.gui.resize(300, 100) self.gui.move(px + (pw / 2) - 150, py + (ph / 2) - 250) else: raise AttributeError("Instant search: Wrong position preference.") self.gui.show_all() self.labelVar = "" self.timeout = "" self.timeoutOpenPage = None # lastPage = "" # pageTitleOnly = False menu = [] # queryTime = 0 def change(self, _): # widget, event,text if self.timeout: GObject.source_remove(self.timeout) q = self.inputEntry.get_text() # print("Change. {} {}".format(input, self.lastQuery)) if q == self.lastQuery: return if q == self.title_match_char: return if q and q[-1] == "∀": # easter egg: debug option for zim --standalone q = q[:-1] import ipdb ipdb.set_trace() self.state = State.setCurrent(q) if not self.state.isFinished: self.isSubset = True if self.lastQuery and q.startswith( self.lastQuery) else False self.state.checkTitleSearch(self.title_match_char) self.startSearch() else: # search completed before # print("Search already cached.") self.startSearch( ) # update the results in a page has been modified meanwhile (not if something got deleted in the notebook #16 ) self.checkLast() self.soutMenu() self.lastQuery = q def startSearch(self): """ Search string has certainly changed. We search in indexed titles and/or we start zim search. Normally, zim gives 11 points bonus if the search-string appears in the titles. If we are ignoring subpages, the search "foo" will match only page "journal:foo", but not "journal:foo:subpage" (and score of the parent page will get slightly higher by 1.) However, if there are occurences of the string in the fulltext of the subpage, subpage remains in the result, but gets bonus only 2 points (not 11). """ query = self.state.query menu = self.state.menu isInQuery = re.compile( r"(^|:|\s|\()" + query ).search # 'te' matches this page titles: 'test' or 'Journal:test' or 'foo test' or 'foo (test)' if self.isSubset and len(query) < self.start_search_length: # letter(s) was/were added and full search has not yet been activated for path in _MenuItem.titles: if path in self.state.menu and not isInQuery( path.lower()): # 'te' didnt match 'test' etc del menu[path] # we pop out the result else: menu[path].sure = True else: # perform new search in cached_titles _MenuItem.titles = set() found = 0 if self.state.firstSeen: for path, pathLow in self.cached_titles: # quick search in titles if isInQuery( pathLow ): # 'te' matches 'test' or 'Journal:test' etc _MenuItem.titles.add(path) if query in path.lower() and query not in path.lower( ).split(":" )[-1]: # "raz" in "raz:dva", but not in "dva" self.state.menu[":".join(path.split( ":")[:-1])].bonus += 1 # 1 point for subpage menu[path].bonus = -11 menu[ path].score += 10 # 10 points for title (zim default) (so that it gets displayed before search finishes) menu[path].intitle = True menu[path].path = path found += 1 if found >= 10: # we dont want more than 10 results; we would easily match all of the pages break else: menu[path].intitle = False self.processMenu() # show for now results of title search if len(query) >= self.start_search_length: self.timeout = GObject.timeout_add( self.keystroke_delay, self.startZimSearch) # ideal delay between keystrokes def startZimSearch(self): """ Starts search for the input. """ self.timeout = "" self.caret['altPos'] = 0 # possible position of caret - beginning s = '"*{}*"'.format( self.state.query ) if self.plugin.preferences['isWildcarded'] else self.state.query self.queryO = Query( s ) # Xunicode(s) beware when searching for unicode character. Update the row when going to Python3. lastSel = self.selection if self.isSubset and self.state.previous.isFinished else None # it should be quicker to find the string, if we provide this subset from last time (in the case we just added a letter, so that the subset gets smaller) self.selection = SearchSelection(self.window.notebook) state = self.state # this is thread, so that self.state would can before search finishes self.selection.search(self.queryO, selection=lastSel, callback=self._search_callback( self.state.rawQuery)) state.isFinished = True for item in list( state.menu ): # remove all the items that we didnt encounter during the search if not state.menu[item].sure: del state.menu[item] if state == self.state: self.checkLast() self.processMenu(state=state) def checkLast(self): """ opens the page if there is only one option in the menu """ if self.open_when_unique and len(self.state.menu) == 1: self._open_page(Path(self.state.menu.keys()[0]), excludeFromHistory=False) self.close() def _search_callback(self, query): def _search_callback(results, path): if results is not None: self._update_results( results, State.get(query) ) # we finish the search even if another search is running. If we returned False, the search would be cancelled- while Gtk.events_pending(): Gtk.main_iteration_do(blocking=False) return True return _search_callback def _update_results(self, results, state): """ This method may run many times, due to the _update_results, which are updated many times. I may set that _update_results would run only once, but this is nice - the results are appearing one by one. """ changed = False state.lastResults = results for option in results.scores: if state.pageTitleOnly and state.query not in option.name: # hledame jen v nazvu stranky continue if option.name not in state.menu: # new item found if state == self.state and option.name == self.caret[ 'text']: # this is current search self.caret['altPos'] = len( state.menu ) - 1 # karet byl na tehle pozici, pokud se zuzil vyber, budeme vedet, kam karet opravne umistit if option.name not in state.menu or ( state.menu[option.name].bonus < 0 and state.menu[option.name].score == 0): changed = True if not state.menu[option.name].sure: state.menu[option.name].sure = True changed = True state.menu[option.name].score = results.scores[ option] # zaradit mezi moznosti if changed: # we added a page self.processMenu(state=state, sort=False) else: pass def processMenu(self, state=None, sort=True): """ Sort menu and generate items and sout menu. """ if state is None: state = self.state if sort: state.items = sorted( state.menu, reverse=True, key=lambda item: (state.menu[item].intitle, state.menu[item].score + state.menu[ item].bonus, -item.count(":"), item)) else: # when search results are being updated, it's good when the order doesnt change all the time. So that the first result does not become for a while 10th and then become first back. state.items = sorted( state.menu, reverse=True, key=lambda item: (state.menu[item].intitle, -state.menu[item].lastOrder)) state.items = [ item for item in state.items if (state.menu[item].score + state.menu[item].bonus) > 0 ] # i dont know why there are items with 0 score if state == self.state: self.soutMenu() def soutMenu(self, displayImmediately=False): """ Displays menu and handles caret position. """ if self.timeoutOpenPage: GObject.source_remove(self.timeoutOpenPage) self.gui.resize(300, 100) # reset size # treat possible caret deflection if self.caret['pos'] < 0 or self.caret['pos'] > len( self.state.items ) - 1: # place the caret to the beginning or the end of list self.caret['pos'] = self.caret['altPos'] text = "" i = 0 for item in self.state.items: score = self.state.menu[item].score + self.state.menu[item].bonus self.state.menu[item].lastOrder = i if i == self.caret['pos']: self.caret['text'] = item # caret is at this position text += '→ {} ({}) {}\n'.format( item, score, "" if self.state.menu[item].sure else "?") else: try: text += '{} ({}) {}\n'.format( item, score, "" if self.state.menu[item].sure else "?") except: text += "CHYBA\n" text += item[0:-1] + "\n" i += 1 self.labelObject.set_text(text) self.menuPage = Path( self.caret['text'] if len(self.state.items) else self.originalPage) if not displayImmediately: self.timeoutOpenPage = GObject.timeout_add( self.keystroke_delay, self._open_page, self.menuPage) # ideal delay between keystrokes else: self._open_page(self.menuPage) def move(self, widget, event): """ Move caret up and down. Enter to confirm, Esc closes search.""" keyname = Gdk.keyval_name(event.keyval) if keyname == "Up" or keyname == "ISO_Left_Tab": self.caret['pos'] -= 1 self.soutMenu(displayImmediately=False) if keyname == "Down" or keyname == "Tab": self.caret['pos'] += 1 self.soutMenu(displayImmediately=False) if keyname == "KP_Enter" or keyname == "Return": self._open_page(self.menuPage, excludeFromHistory=False) self.close() if keyname == "Escape": self._open_original() # GTK closes the windows itself on Escape, no self.close() needed return ## Safely closes # Xwhen closing directly, Python gave allocation error def close(self): if not self.isClosed: self.isClosed = True self.gui.emit("close") def _open_original(self): self._open_page(Path(self.originalPage)) def _open_page(self, page, excludeFromHistory=True): """ Open page and highlight matches """ self.timeoutOpenPage = None # no delayed page will be open if self.isClosed == True: return if page and page.name and page.name != self.lastPage: self.lastPage = page.name # print("*** HISTORY BEF", self.window.ui.history._history[-3:]) self.window.open_page(page) if excludeFromHistory: # there is no public API, so lets use protected _history instead self.window.history._history.pop() self.window.history._current = len( self.window.history._history) - 1 if not excludeFromHistory and self.window.history.get_current( ).name is not page.name: # we insert the page to the history because it was likely to be just visited and excluded self.window.history.append(page) # Popup find dialog with same query if self.queryO: # and self.queryO.simple_match: string = self.state.query string = string.strip('*') # support partial matches if self.plugin.preferences['highlight_search']: self.window.pageview.show_find(string, highlight=True)