class Entry(object): # These are labels for possible states of version controlled files; # not all states have a label to avoid visual clutter. state_names = { STATE_IGNORED: _("Ignored"), STATE_NONE: _("Unversioned"), STATE_NORMAL: "", STATE_NOCHANGE: "", STATE_ERROR: _("Error"), STATE_EMPTY: "", STATE_NEW: _("Newly added"), STATE_MODIFIED: _("Modified"), STATE_RENAMED: _("Renamed"), STATE_CONFLICT: "<b>%s</b>" % _("Conflict"), STATE_REMOVED: _("Removed"), STATE_MISSING: _("Missing"), STATE_NONEXIST: _("Not present"), } def __init__(self, path, name, state): self.path = path self.state = state self.parent, self.name = os.path.split(path.rstrip("/")) def __str__(self): return "<%s:%s %s>" % (self.__class__.__name__, self.path, self.get_status() or "Normal") def __repr__(self): return "%s(%r, %r, %r)" % (self.__class__.__name__, self.name, self.path, self.state) def get_status(self): return self.state_names[self.state]
class Entry(object): # These are labels for possible states of version controlled files; # not all states have a label to avoid visual clutter. state_names = { STATE_IGNORED: _("Ignored"), STATE_NONE: _("Unversioned"), STATE_NORMAL: "", STATE_NOCHANGE: "", STATE_ERROR: _("Error"), STATE_EMPTY: "", STATE_NEW: _("Newly added"), STATE_MODIFIED: _("Modified"), STATE_RENAMED: _("Renamed"), STATE_CONFLICT: "<b>%s</b>" % _("Conflict"), STATE_REMOVED: _("Removed"), STATE_MISSING: _("Missing"), STATE_NONEXIST: _("Not present"), } def __init__(self, path, name, state, isdir, options=None): self.path = path self.name = name self.state = state self.isdir = isdir if isinstance(options, list): options = ','.join(options) self.options = options def __str__(self): return "<%s:%s %s>" % (self.__class__.__name__, self.path, self.get_status() or "Normal") def __repr__(self): return "%s(%r, %r, %r)" % (self.__class__.__name__, self.name, self.path, self.state) def get_status(self): return self.state_names[self.state] def is_present(self): """Should this Entry actually be present on the file system""" return self.state not in (STATE_REMOVED, STATE_MISSING) @staticmethod def is_modified(entry): return entry.state >= STATE_NEW or (entry.isdir and (entry.state > STATE_NONE)) @staticmethod def is_normal(entry): return entry.state == STATE_NORMAL @staticmethod def is_nonvc(entry): return entry.state == STATE_NONE or (entry.isdir and (entry.state > STATE_IGNORED)) @staticmethod def is_ignored(entry): return entry.state == STATE_IGNORED or entry.isdir
def setup_mac_integration(self, menubar): self.set_use_quartz_accelerators(True) self.set_menu_bar(menubar) item = Gtk.MenuItem.new_with_label(_("About")) item.connect("activate", self.about_callback, None) menubar.add(item) self.insert_app_menu_item(item, 0) self.set_about_item(item) separator = Gtk.SeparatorMenuItem() menubar.add(separator) self.insert_app_menu_item(separator, 1) item = Gtk.MenuItem.new_with_label(_("Preferences")) item.connect("activate", self.preferences_callback, None) menubar.add(item) self.insert_app_menu_item(item, 2) item = Gtk.MenuItem.new_with_label(_("Shell Integration")) item.connect("activate", self.mac_shell_integration_callback, None) menubar.add(item) self.insert_app_menu_item(item, 3) separator = Gtk.SeparatorMenuItem() menubar.add(separator) self.insert_app_menu_item(separator, 4) self.sync_menubar() self.ready()
def __init__(self): GObject.GObject.__init__(self) self.scheduler = task.FifoScheduler() self.num_panes = 0 self.label_text = _("untitled") self.tooltip_text = _("untitled") self.main_actiongroup = None
def on_button_remove_clicked(self, obj): selected = self._get_selected_files() if any(os.path.isdir(p) for p in selected): # TODO: Improve and reuse this dialog for the non-VC delete action dialog = Gtk.MessageDialog( parent=self.widget.get_toplevel(), flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, type=Gtk.MessageType.WARNING, message_format=_("Remove folder and all its files?")) dialog.format_secondary_text( _("This will remove all selected files and folders, and all " "files within any selected folders, from version control.")) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.add_button(_("_Remove"), Gtk.ResponseType.OK) response = dialog.run() dialog.destroy() if response != Gtk.ResponseType.OK: return try: self.vc.remove(self._command, selected) except NotImplementedError: self._command_on_selected(self.vc.remove_command())
def setup_mac_integration(self, menubar): from Cocoa import NSApp self.set_use_quartz_accelerators(True) self.set_menu_bar(menubar) item = Gtk.MenuItem.new_with_label(_("About")) item.connect("activate", self.about_callback, None) menubar.add(item) self.insert_app_menu_item(item, 0) self.set_about_item(item) separator = Gtk.SeparatorMenuItem() menubar.add(separator) self.insert_app_menu_item(separator, 1) item = Gtk.MenuItem.new_with_label(_("Preferences")) item.connect("activate", self.preferences_callback, None) menubar.add(item) self.insert_app_menu_item(item, 2) item = Gtk.MenuItem.new_with_label(_("Shell Integration")) item.connect("activate", self.mac_shell_integration_callback, None) menubar.add(item) self.insert_app_menu_item(item, 3) separator = Gtk.SeparatorMenuItem() menubar.add(separator) self.insert_app_menu_item(separator, 4) self.sync_menubar() self.ready() NSApp.activateIgnoringOtherApps_(True) self.attention_request(GtkosxApplication.ApplicationAttentionType.NFO_REQUEST)
def recompute_label(self): location = self.location if isinstance(location, str): location = location.decode(sys.getfilesystemencoding(), 'replace') self.label_text = os.path.basename(location) # TRANSLATORS: This is the location of the directory the user is diffing self.tooltip_text = _("%s: %s") % (_("Location"), location) self.label_changed()
def populate_vcs_for_location(self, location): """Display VC plugin(s) that can handle the location""" vcs_model = self.combobox_vcs.get_model() vcs_model.clear() # VC systems can be executed at the directory level, so make sure # we're checking for VC support there instead of # on a specific file or on deleted/unexisting path inside vc location = os.path.abspath(location or ".") while not os.path.isdir(location): parent_location = os.path.dirname(location) if len(parent_location) >= len(location): # no existing parent: for example unexisting drive on Windows break location = parent_location else: # existing parent directory was found for avc in get_vcs(location): err_str = '' vc_details = {'name': avc.NAME, 'cmd': avc.CMD} if not avc.is_installed(): # Translators: This error message is shown when a version # control binary isn't installed. err_str = _("%(name)s (%(cmd)s not installed)") elif not avc.valid_repo(location): # Translators: This error message is shown when a version # controlled repository is invalid. err_str = _("%(name)s (Invalid repository)") if err_str: vcs_model.append([err_str % vc_details, avc, False]) continue vcs_model.append([avc.NAME, avc(location), True]) valid_vcs = [(i, r[1].NAME) for i, r in enumerate(vcs_model) if r[2]] default_active = min(valid_vcs)[0] if valid_vcs else 0 # Keep the same VC plugin active on refresh, otherwise use the first current_vc_name = self.vc.NAME if self.vc else None same_vc = [i for i, name in valid_vcs if name == current_vc_name] if same_vc: default_active = same_vc[0] if not valid_vcs: # If we didn't get any valid vcs then fallback to null null_vcs = _null.Vc(location) vcs_model.insert(0, [null_vcs.NAME, null_vcs, True]) tooltip = _("No valid version control system found in this folder") elif len(vcs_model) == 1: tooltip = _("Only one version control system found in this folder") else: tooltip = _("Choose which version control system to use") self.combobox_vcs.set_tooltip_text(tooltip) self.combobox_vcs.set_sensitive(len(vcs_model) > 1) self.combobox_vcs.set_active(default_active)
def populate_vcs_for_location(self, location): """Display VC plugin(s) that can handle the location""" vcs_model = self.combobox_vcs.get_model() vcs_model.clear() # VC systems can be executed at the directory level, so make sure # we're checking for VC support there instead of # on a specific file or on deleted/unexisting path inside vc location = os.path.abspath(location or ".") while not os.path.isdir(location): parent_location = os.path.dirname(location) if len(parent_location) >= len(location): # no existing parent: for example unexisting drive on Windows break location = parent_location else: # existing parent directory was found for avc in vc.get_vcs(location): err_str = '' vc_details = {'name': avc.NAME, 'cmd': avc.CMD} if not avc.is_installed(): # Translators: This error message is shown when a version # control binary isn't installed. err_str = _("%(name)s (%(cmd)s not installed)") elif not avc.valid_repo(location): # Translators: This error message is shown when a version # controlled repository is invalid. err_str = _("%(name)s (Invalid repository)") if err_str: vcs_model.append([err_str % vc_details, avc, False]) continue vcs_model.append([avc.NAME, avc(location), True]) valid_vcs = [(i, r[1].NAME) for i, r in enumerate(vcs_model) if r[2]] default_active = min(valid_vcs)[0] if valid_vcs else 0 # Keep the same VC plugin active on refresh, otherwise use the first current_vc_name = self.vc.NAME if self.vc else None same_vc = [i for i, name in valid_vcs if name == current_vc_name] if same_vc: default_active = same_vc[0] if not valid_vcs: # If we didn't get any valid vcs then fallback to null null_vcs = _null.Vc(location) vcs_model.insert(0, [null_vcs.NAME, null_vcs, True]) tooltip = _("No valid version control system found in this folder") elif len(vcs_model) == 1: tooltip = _("Only one version control system found in this folder") else: tooltip = _("Choose which version control system to use") self.combobox_vcs.set_tooltip_text(tooltip) self.combobox_vcs.set_sensitive(len(vcs_model) > 1) self.combobox_vcs.set_active(default_active)
def recompute_label(self): location = self.location if isinstance(location, str): location = location.decode( sys.getfilesystemencoding(), 'replace') self.label_text = os.path.basename(location) # TRANSLATORS: This is the location of the directory the user is diffing self.tooltip_text = _("%s: %s") % (_("Location"), location) self.label_changed()
def _search_recursively_iter(self, iterstart): rootname = self.model.value_path(iterstart, 0) prefixlen = len(self.location) + 1 symlinks_followed = set() todo = [(self.model.get_path(iterstart), rootname)] flattened = self.actiongroup.get_action("VcFlatten").get_active() active_action = lambda a: self.actiongroup.get_action(a).get_active() filters = [a[1] for a in self.state_actions.values() if active_action(a[0]) and a[1]] yield _("Scanning %s") % rootname self.vc.cache_inventory(rootname) while todo: # This needs to happen sorted and depth-first in order for our row # references to remain valid while we traverse. todo.sort() treepath, path = todo.pop(0) it = self.model.get_iter(treepath) yield _("Scanning %s") % path[prefixlen:] entries = self.vc.listdir(path) entries = [e for e in entries if any(f(e) for f in filters)] for e in entries: if e.isdir: try: st = os.lstat(e.path) # Covers certain unreadable symlink cases; see bgo#585895 except OSError as err: error_string = "%s: %s" % (e.path, err.strerror) self.model.add_error(it, error_string, 0) continue if stat.S_ISLNK(st.st_mode): key = (st.st_dev, st.st_ino) if key in symlinks_followed: continue symlinks_followed.add(key) if flattened: if e.state != tree.STATE_IGNORED: todo.append((Gtk.TreePath.new_first(), e.path)) continue child = self.model.add_entries(it, [e.path]) if e.isdir and e.state != tree.STATE_IGNORED: todo.append((self.model.get_path(child), e.path)) self._update_item_state(child, e, path[prefixlen:]) if flattened: root = Gtk.TreePath.new_first() self.treeview.expand_row(Gtk.TreePath(root), False) else: if not entries: self.model.add_empty(it, _("(Empty)")) if any(e.state != tree.STATE_NORMAL for e in entries): self.treeview.expand_to_path(treepath)
def file_saved_cb(self, saver, result, *args): gfile = saver.get_location() try: saver.save_finish(result) except GLib.Error as err: filename = GLib.markup_escape_text(gfile.get_parse_name()) error_dialog( primary=_("Could not save file %s.") % filename, secondary=_("Couldn’t save file due to:\n%s") % (GLib.markup_escape_text(str(err))), )
def file_saved_cb(self, saver, result, *args): gfile = saver.get_location() try: saver.save_finish(result) except GLib.Error as err: filename = GLib.markup_escape_text(gfile.get_parse_name()) error_dialog( primary=_("Could not save file %s.") % filename, secondary=_("Couldn’t save file due to:\n%s") % ( GLib.markup_escape_text(str(err))), )
def populate_vcs_for_location(self, location): """Display VC plugin(s) that can handle the location""" vcs_model = self.combobox_vcs.get_model() vcs_model.clear() # VC systems can be executed at the directory level, so make sure # we're checking for VC support there instead of # on a specific file or on deleted/unexisting path inside vc location = os.path.abspath(location or ".") while not os.path.isdir(location): parent_location = os.path.dirname(location) if len(parent_location) >= len(location): # no existing parent: for example unexisting drive on Windows break location = parent_location else: # existing parent directory was found for avc, enabled in get_vcs(location): err_str = '' vc_details = {'name': avc.NAME, 'cmd': avc.CMD} if not enabled: # Translators: This error message is shown when no # repository of this type is found. err_str = _("%(name)s (not found)") elif not avc.is_installed(): # Translators: This error message is shown when a version # control binary isn't installed. err_str = _("%(name)s (%(cmd)s not installed)") elif not avc.valid_repo(location): # Translators: This error message is shown when a version # controlled repository is invalid. err_str = _("%(name)s (invalid repository)") if err_str: vcs_model.append([err_str % vc_details, avc, False]) continue vcs_model.append([avc.NAME, avc(location), True]) default_active = self.get_default_vc(vcs_model) if not any(enabled for _, _, enabled in vcs_model): # If we didn't get any valid vcs then fallback to null null_vcs = _null.Vc(location) vcs_model.insert(0, [null_vcs.NAME, null_vcs, True]) tooltip = _("No valid version control system found in this folder") else: tooltip = _("Choose which version control system to use") self.combobox_vcs.set_tooltip_text(tooltip) self.combobox_vcs.set_active(default_active)
def _make_copy_menu(self, chunk): copy_menu = Gtk.Menu() copy_up = Gtk.MenuItem.new_with_mnemonic(_('Copy _up')) copy_down = Gtk.MenuItem.new_with_mnemonic(_('Copy _down')) copy_menu.append(copy_up) copy_menu.append(copy_down) copy_menu.show_all() def copy_chunk(widget, action): self._action_on_chunk(action, chunk) copy_up.connect('activate', copy_chunk, ChunkAction.copy_up) copy_down.connect('activate', copy_chunk, ChunkAction.copy_down) return copy_menu
def prompt_save_filename(title: str, parent: Optional[Gtk.Widget] = None ) -> Optional[Gio.File]: dialog = MeldFileChooserDialog( title, transient_for=get_modal_parent(parent), action=Gtk.FileChooserAction.SAVE, ) dialog.set_default_response(Gtk.ResponseType.ACCEPT) response = dialog.run() gfile = dialog.get_file() dialog.destroy() if response != Gtk.ResponseType.ACCEPT or not gfile: return None try: file_info = gfile.query_info( 'standard::name,standard::display-name', Gio.FileQueryInfoFlags.NONE, None, ) except GLib.Error as err: if err.code == Gio.IOErrorEnum.NOT_FOUND: return gfile raise # The selected file exists, so we need to prompt for overwrite. parent_folder = gfile.get_parent() parent_name = parent_folder.get_parse_name() if parent_folder else '' file_name = file_info.get_display_name() replace = modal_dialog( primary=_("Replace file “%s”?") % file_name, secondary=_("A file with this name already exists in “%s”.\n" "If you replace the existing file, its contents " "will be lost.") % parent_name, buttons=[ (_("_Cancel"), Gtk.ResponseType.CANCEL), (_("_Replace"), Gtk.ResponseType.OK), ], messagetype=Gtk.MessageType.WARNING, ) if replace != Gtk.ResponseType.OK: return None return gfile
def _update_notebook_menu(self, *args): if self.tab_switch_merge_id: self.ui.remove_ui(self.tab_switch_merge_id) self.ui.remove_action_group(self.tab_switch_actiongroup) self.tab_switch_merge_id = self.ui.new_merge_id() self.tab_switch_actiongroup = Gtk.ActionGroup(name="TabSwitchActions") self.ui.insert_action_group(self.tab_switch_actiongroup) group = None current_page = self.notebook.get_current_page() for i in range(self.notebook.get_n_pages()): page = self.notebook.get_nth_page(i) label = self.notebook.get_menu_label_text(page) or "" label = label.replace("_", "__") name = "SwitchTab%d" % i tooltip = _("Switch to this tab") action = Gtk.RadioAction(name=name, label=label, tooltip=tooltip, stock_id=None, value=i) action.join_group(group) group = action action.set_active(current_page == i) def current_tab_changed_cb(action, current): if action == current: self.notebook.set_current_page(action.get_current_value()) action.connect("changed", current_tab_changed_cb) if i < 10: accel = "<Alt>%d" % ((i + 1) % 10) else: accel = None self.tab_switch_actiongroup.add_action_with_accel(action, accel) self.ui.add_ui(self.tab_switch_merge_id, "/Menubar/TabMenu/TabPlaceholder", name, name, Gtk.UIManagerItemType.MENUITEM, False)
def shorten_names(*names: str) -> List[str]: """Remove common parts of a list of paths For example, `('/tmp/foo1', '/tmp/foo2')` would be summarised as `('foo1', 'foo2')`. Paths that share a basename are distinguished by prepending an indicator, e.g., `('/a/b/c', '/a/d/c')` would be summarised to `['[b] c', '[d] c']`. """ paths = [PurePath(n) for n in names] # Identify the longest common path among the list of path common = set(paths[0].parents) common = common.intersection(*(p.parents for p in paths)) if not common: return list(names) common_parent = sorted(common, key=lambda p: -len(p.parts))[0] paths = [p.relative_to(common_parent) for p in paths] basenames = [p.name for p in paths] if all_same(basenames): def firstpart(path: PurePath) -> str: if len(path.parts) > 1 and path.parts[0]: return "[%s] " % path.parts[0] else: return "" return [firstpart(p) + p.name for p in paths] return [name or _("[None]") for name in basenames]
def colour_lookup_with_fallback(name, attribute): from meld.settings import meldsettings source_style = meldsettings.style_scheme style = source_style.get_style(name) style_attr = getattr(style.props, attribute) if style else None if not style or not style_attr: manager = GtkSource.StyleSchemeManager.get_default() source_style = manager.get_scheme(MELD_STYLE_SCHEME) try: style = source_style.get_style(name) style_attr = getattr(style.props, attribute) except AttributeError: pass if not style_attr: import sys print >> sys.stderr, _( "Couldn't find colour scheme details for %s-%s; " "this is a bad install") % (name, attribute) sys.exit(1) colour = Gdk.RGBA() colour.parse(style_attr) return colour
def run(self): self.update_patch() while 1: result = self.widget.run() if result < 0: break buf = self.textview.get_buffer() start, end = buf.get_bounds() txt = text_type(buf.get_text(start, end, False), 'utf8') # Copy patch to clipboard if result == 1: clip = Gtk.clipboard_get() clip.set_text(txt) clip.store() break # Save patch as a file else: # FIXME: These filediff methods are actually general utility. filename = self.filediff._get_filename_for_saving( _("Save Patch")) if filename: txt = txt.encode('utf-8') self.filediff._save_text_to_filename(filename, txt) break self.widget.hide()
def shorten_names(*names): """Remove redunant parts of a list of names (e.g. /tmp/foo{1,2} -> foo{1,2} """ # TODO: Update for different path separators and URIs prefix = os.path.commonprefix(names) prefixslash = prefix.rfind("/") + 1 names = [n[prefixslash:] for n in names] paths = [n.split("/") for n in names] try: basenames = [p[-1] for p in paths] except IndexError: pass else: if all_same(basenames): def firstpart(alist): if len(alist) > 1 and alist[0]: return "[%s] " % alist[0] else: return "" roots = [firstpart(p) for p in paths] base = basenames[0].strip() return [r + base for r in roots] # no common path. empty names get changed to "[None]" return [name or _("[None]") for name in basenames]
def append_diff(self, gfiles, auto_compare=False, auto_merge=False, merge_output=None, meta=None): have_directories = False have_files = False for f in gfiles: if f.query_file_type(Gio.FileQueryInfoFlags.NONE, None) == Gio.FileType.DIRECTORY: have_directories = True else: have_files = True if have_directories and have_files: raise ValueError( _("Cannot compare a mixture of files and directories")) elif have_directories: return self.append_dirdiff(gfiles, auto_compare) elif auto_merge: return self.append_filemerge(gfiles, merge_output=merge_output) else: return self.append_filediff(gfiles, merge_output=merge_output, meta=meta)
def _update_notebook_menu(self, *args): if self.tab_switch_merge_id: self.ui.remove_ui(self.tab_switch_merge_id) self.ui.remove_action_group(self.tab_switch_actiongroup) self.tab_switch_merge_id = self.ui.new_merge_id() self.tab_switch_actiongroup = Gtk.ActionGroup(name="TabSwitchActions") self.ui.insert_action_group(self.tab_switch_actiongroup) group = None current_page = self.notebook.get_current_page() for i in range(self.notebook.get_n_pages()): page = self.notebook.get_nth_page(i) label = self.notebook.get_menu_label_text(page) or "" name = "SwitchTab%d" % i tooltip = _("Switch to this tab") action = Gtk.RadioAction(name=name, label=label, tooltip=tooltip, stock_id=None, value=i) action.join_group(group) group = action action.set_active(current_page == i) def current_tab_changed_cb(action, current): if action == current: self.notebook.set_current_page(action.get_current_value()) action.connect("changed", current_tab_changed_cb) if i < 10: accel = "<Alt>%d" % ((i + 1) % 10) else: accel = None self.tab_switch_actiongroup.add_action_with_accel(action, accel) self.ui.add_ui(self.tab_switch_merge_id, "/Menubar/TabMenu/TabPlaceholder", name, name, Gtk.UIManagerItemType.MENUITEM, False)
def get_commits_to_push_summary(self): branch_refs = self.get_commits_to_push() unpushed_branches = len([v for v in branch_refs.values() if v]) unpushed_commits = sum(len(v) for v in branch_refs.values()) if unpushed_commits: if unpushed_branches > 1: # Translators: First element is replaced by translated "%d # unpushed commits", second element is replaced by translated # "%d branches" label = _("{unpushed_commits} in {unpushed_branches}").format( unpushed_commits=ngettext( "%d unpushed commit", "%d unpushed commits", unpushed_commits) % unpushed_commits, unpushed_branches=ngettext("%d branch", "%d branches", unpushed_branches) % unpushed_branches, ) else: # Translators: These messages cover the case where there is # only one branch, and are not part of another message. label = ngettext("%d unpushed commit", "%d unpushed commits", unpushed_commits) % (unpushed_commits) else: label = "" return label
def _update_tree_state_cache(self, path): while 1: try: proc = _vc.popen( [self.CMD, "status", "-v", "--xml", path], cwd=self.location) tree = ElementTree.parse(proc) break except OSError as e: if e.errno != errno.EAGAIN: raise for target in tree.findall("target") + tree.findall("changelist"): for entry in (t for t in target.getchildren() if t.tag == "entry"): path = entry.attrib["path"] if not path: continue if not os.path.isabs(path): path = os.path.abspath(os.path.join(self.location, path)) for status in (e for e in entry.getchildren() \ if e.tag == "wc-status"): item = status.attrib["item"] if item == "": continue state = self.state_map.get(item, _vc.STATE_NONE) self._tree_cache[path] = state rev = status.attrib.get("revision") rev_label = _("Rev %s") % rev if rev is not None else '' self._tree_meta_cache[path] = rev_label
def append_diff( self, gfiles: Sequence[Optional[Gio.File]], auto_compare: bool = False, auto_merge: bool = False, merge_output: Optional[Gio.File] = None, meta: Optional[Dict[str, Any]] = None, ): have_directories = False have_files = False for f in gfiles: if not f: continue file_type = f.query_file_type(Gio.FileQueryInfoFlags.NONE, None) if file_type == Gio.FileType.DIRECTORY: have_directories = True else: have_files = True if have_directories and have_files: raise ValueError( _("Cannot compare a mixture of files and directories")) elif have_directories: return self.append_dirdiff(gfiles, auto_compare) elif auto_merge: return self.append_filemerge(gfiles, merge_output=merge_output) else: return self.append_filediff(gfiles, merge_output=merge_output, meta=meta)
def _update_tree_state_cache(self, path): while 1: try: proc = _vc.popen( [self.CMD, "status", "-v", "--xml", path], cwd=self.location) tree = ElementTree.parse(proc) break except OSError as e: if e.errno != errno.EAGAIN: raise for target in tree.findall("target") + tree.findall("changelist"): for entry in (t for t in target.getchildren() if t.tag == "entry"): path = entry.attrib["path"] if not path: continue if not os.path.isabs(path): path = os.path.abspath(os.path.join(self.location, path)) for status in (e for e in entry.getchildren() if e.tag == "wc-status"): item = status.attrib["item"] if item == "": continue state = self.state_map.get(item, _vc.STATE_NONE) self._tree_cache[path] = state rev = status.attrib.get("revision") rev_label = _("Rev %s") % rev if rev is not None else '' self._tree_meta_cache[path] = rev_label self._add_missing_cache_entry(path, state)
def shorten_names(*names): """Remove redunant parts of a list of names (e.g. /tmp/foo{1,2} -> foo{1,2} """ # TODO: Update for different path separators prefix = os.path.commonprefix(names) prefixslash = prefix.rfind("/") + 1 names = [n[prefixslash:] for n in names] paths = [n.split("/") for n in names] try: basenames = [p[-1] for p in paths] except IndexError: pass else: if all_same(basenames): def firstpart(alist): if len(alist) > 1: return "[%s] " % alist[0] else: return "" roots = [firstpart(p) for p in paths] base = basenames[0].strip() return [r + base for r in roots] # no common path. empty names get changed to "[None]" return [name or _("[None]") for name in basenames]
def _update_tree_state_cache(self, path, tree_state): """ Update the state of the file(s) at tree_state['path'] """ while 1: try: entries = self._get_modified_files(path) # Identify ignored files and folders proc = _vc.popen([self.CMD, "ls-files", "--others", "--ignored", "--exclude-standard", "--directory", path], cwd=self.location) ignored_entries = proc.read().split("\n")[:-1] # Identify unversioned files proc = _vc.popen([self.CMD, "ls-files", "--others", "--exclude-standard", path], cwd=self.location) unversioned_entries = proc.read().split("\n")[:-1] break except OSError as e: if e.errno != errno.EAGAIN: raise if len(entries) == 0 and os.path.isfile(path): # If we're just updating a single file there's a chance that it # was it was previously modified, and now has been edited # so that it is un-modified. This will result in an empty # 'entries' list, and tree_state['path'] will still contain stale # data. When this corner case occurs we force tree_state['path'] # to STATE_NORMAL. path = os.path.abspath(path) tree_state[path] = _vc.STATE_NORMAL else: # There are 1 or more modified files, parse their state for entry in entries: columns = self.DIFF_RE.search(entry).groups() old_mode, new_mode, statekey, name = columns if os.name == 'nt': # Git returns unix-style paths on Windows name = os.path.normpath(name.strip()) path = os.path.join(self.root, name.strip()) path = os.path.abspath(path) state = self.state_map.get(statekey.strip(), _vc.STATE_NONE) tree_state[path] = state if old_mode != new_mode: msg = _("Mode changed from %s to %s" % (old_mode, new_mode)) self._tree_meta_cache[path] = msg for entry in ignored_entries: path = os.path.join(self.location, entry.strip()) path = os.path.abspath(path) tree_state[path] = _vc.STATE_IGNORED for entry in unversioned_entries: path = os.path.join(self.location, entry.strip()) path = os.path.abspath(path) tree_state[path] = _vc.STATE_NONE
def _update_tree_state_cache(self, path): """ Update the state of the file(s) at self._tree_cache['path'] """ while 1: try: entries = self._get_modified_files(path) # Identify ignored files and folders proc = self.run( "ls-files", "--others", "--ignored", "--exclude-standard", "--directory", path) ignored_entries = proc.stdout.read().split("\n")[:-1] # Identify unversioned files proc = self.run( "ls-files", "--others", "--exclude-standard", path) unversioned_entries = proc.stdout.read().split("\n")[:-1] break except OSError as e: if e.errno != errno.EAGAIN: raise def get_real_path(name): name = name.strip() if os.name == 'nt': # Git returns unix-style paths on Windows name = os.path.normpath(name) # Unicode file names and file names containing quotes are # returned by git as quoted strings if name[0] == '"': name = name[1:-1].decode('string_escape') return os.path.abspath( os.path.join(self.location, name)) if len(entries) == 0 and os.path.isfile(path): # If we're just updating a single file there's a chance that it # was it was previously modified, and now has been edited so that # it is un-modified. This will result in an empty 'entries' list, # and self._tree_cache['path'] will still contain stale data. # When this corner case occurs we force self._tree_cache['path'] # to STATE_NORMAL. self._tree_cache[get_real_path(path)] = _vc.STATE_NORMAL else: for entry in entries: columns = self.DIFF_RE.search(entry).groups() old_mode, new_mode, statekey, path = columns state = self.state_map.get(statekey.strip(), _vc.STATE_NONE) self._tree_cache[get_real_path(path)] = state if old_mode != new_mode: msg = _("Mode changed from %s to %s" % (old_mode, new_mode)) self._tree_meta_cache[path] = msg for path in ignored_entries: self._tree_cache[get_real_path(path)] = _vc.STATE_IGNORED for path in unversioned_entries: self._tree_cache[get_real_path(path)] = _vc.STATE_NONE
class LabeledObjectMixin(GObject.GObject): label_text = _("untitled") tooltip_text = None @GObject.Signal def label_changed(self, label_text: str, tooltip_text: str) -> None: ...
def on_consoleview_populate_popup(self, textview, menu): buf = textview.get_buffer() clear_cb = lambda *args: buf.delete(*buf.get_bounds()) clear_action = Gtk.MenuItem.new_with_label(_("Clear")) clear_action.connect("activate", clear_cb) menu.insert(clear_action, 0) menu.insert(Gtk.SeparatorMenuItem(), 1) menu.show_all()
def run_diff(self, path): if os.path.isdir(path): self.emit("create-diff", [path], {}) return left_is_local = self.props.left_is_local basename = self.display_path(os.path.basename(path)) meta = { 'parent': self, 'prompt_resolve': False, } # May have removed directories in list. vc_entry = self.vc.get_entry(path) if vc_entry and vc_entry.state == tree.STATE_CONFLICT and \ hasattr(self.vc, 'get_path_for_conflict'): local_label = _(u"%s — local") % basename remote_label = _(u"%s — remote") % basename # We create new temp files for other, base and this, and # then set the output to the current file. if self.props.merge_file_order == "local-merge-remote": conflicts = (tree.CONFLICT_THIS, tree.CONFLICT_MERGED, tree.CONFLICT_OTHER) meta['labels'] = (local_label, None, remote_label) meta['tablabel'] = _(u"%s (local, merge, remote)") % basename else: conflicts = (tree.CONFLICT_OTHER, tree.CONFLICT_MERGED, tree.CONFLICT_THIS) meta['labels'] = (remote_label, None, local_label) meta['tablabel'] = _(u"%s (remote, merge, local)") % basename diffs = [self.vc.get_path_for_conflict(path, conflict=c) for c in conflicts] temps = [p for p, is_temp in diffs if is_temp] diffs = [p for p, is_temp in diffs] kwargs = { 'auto_merge': False, 'merge_output': path, } meta['prompt_resolve'] = True else: remote_label = _(u"%s — repository") % basename comp_path = self.vc.get_path_for_repo_file(path) temps = [comp_path] if left_is_local: diffs = [path, comp_path] meta['labels'] = (None, remote_label) meta['tablabel'] = _(u"%s (working, repository)") % basename else: diffs = [comp_path, path] meta['labels'] = (remote_label, None) meta['tablabel'] = _(u"%s (repository, working)") % basename kwargs = {} kwargs['meta'] = meta for temp_file in temps: os.chmod(temp_file, 0o444) _temp_files.append(temp_file) self.emit("create-diff", diffs, kwargs)
def setup_integration(self): if self.is_alias_found(): add_shortcut = modal_dialog( primary=_("Mac Shell Integration already exists"), secondary=_("Overwrite alias for meld?" "\n\n*Note*: alias already exists "), buttons=[ (_("_Cancel"), Gtk.ResponseType.CANCEL), (_("Overwrite"), Gtk.ResponseType.OK), ], messagetype=Gtk.MessageType.QUESTION ) else: add_shortcut = Gtk.ResponseType.OK if add_shortcut == Gtk.ResponseType.OK: try: self.create_shell_alias() modal_dialog( primary=_( "Alias created" ), secondary=_( "You should be able to use meld from the command line.\n\n" "New Terminals will work automatically. For Terminals that are already open, issue the command:\n\n" "source ~/.bashrc" ), buttons=[ (_("OK"), Gtk.ResponseType.OK), ], messagetype=Gtk.MessageType.INFO ) except: modal_dialog( primary=_( "Failed to create/update alias" ), secondary=_( "Meld was unable to create the alias required for shell operation. " "Edit your ~/.bashrc and add the line: alias meld={}".format(self.executable_path) ), buttons=[ (_("OK"), Gtk.ResponseType.OK), ], messagetype=Gtk.MessageType.WARNING )
def run_diff(self, path): if os.path.isdir(path): self.emit("create-diff", [path], {}) return left_is_local = self.props.left_is_local basename = os.path.basename(path) meta = { 'parent': self, 'prompt_resolve': False, } # May have removed directories in list. vc_entry = self.vc.get_entry(path) if vc_entry and vc_entry.state == tree.STATE_CONFLICT and \ hasattr(self.vc, 'get_path_for_conflict'): local_label = _(u"%s — local") % basename remote_label = _(u"%s — remote") % basename # We create new temp files for other, base and this, and # then set the output to the current file. if self.props.merge_file_order == "local-merge-remote": conflicts = (tree.CONFLICT_THIS, tree.CONFLICT_MERGED, tree.CONFLICT_OTHER) meta['labels'] = (local_label, None, remote_label) meta['tablabel'] = _(u"%s (local, merge, remote)") % basename else: conflicts = (tree.CONFLICT_OTHER, tree.CONFLICT_MERGED, tree.CONFLICT_THIS) meta['labels'] = (remote_label, None, local_label) meta['tablabel'] = _(u"%s (remote, merge, local)") % basename diffs = [self.vc.get_path_for_conflict(path, conflict=c) for c in conflicts] temps = [p for p, is_temp in diffs if is_temp] diffs = [p for p, is_temp in diffs] kwargs = { 'auto_merge': False, 'merge_output': path, } meta['prompt_resolve'] = True else: remote_label = _(u"%s — repository") % basename comp_path = self.vc.get_path_for_repo_file(path) temps = [comp_path] if left_is_local: diffs = [path, comp_path] meta['labels'] = (None, remote_label) meta['tablabel'] = _(u"%s (working, repository)") % basename else: diffs = [comp_path, path] meta['labels'] = (remote_label, None) meta['tablabel'] = _(u"%s (repository, working)") % basename kwargs = {} kwargs['meta'] = meta for temp_file in temps: os.chmod(temp_file, 0o444) _temp_files.append(temp_file) self.emit("create-diff", diffs, kwargs)
def append_filemerge(self, files, merge_output=None): if len(files) != 3: raise ValueError(_("Need three files to auto-merge, got: %r") % files) doc = filemerge.FileMerge(len(files)) self._append_page(doc, "text-x-generic") doc.set_files(files) if merge_output is not None: doc.set_merge_output_file(merge_output) return doc
def _make_copy_menu(self, chunk): copy_menu = Gtk.Menu() copy_up = Gtk.MenuItem.new_with_mnemonic(_("Copy _up")) copy_down = Gtk.MenuItem.new_with_mnemonic(_("Copy _down")) copy_menu.append(copy_up) copy_menu.append(copy_down) copy_menu.show_all() # FIXME: This is horrible copy_menu.attach_to_widget(self.filediff, None) def copy_chunk(widget, chunk, copy_up): self.filediff.copy_chunk(self.from_pane, self.to_pane, chunk, copy_up) copy_up.connect('activate', copy_chunk, chunk, True) copy_down.connect('activate', copy_chunk, chunk, False) return copy_menu
def prompt_save_filename( title: str, parent: Optional[Gtk.Widget] = None) -> Optional[Gio.File]: dialog = MeldFileChooserDialog( title, transient_for=get_modal_parent(parent), action=Gtk.FileChooserAction.SAVE, ) dialog.set_default_response(Gtk.ResponseType.ACCEPT) response = dialog.run() gfile = dialog.get_file() dialog.destroy() if response != Gtk.ResponseType.ACCEPT or not gfile: return None try: file_info = gfile.query_info( 'standard::name,standard::display-name', 0, None) except GLib.Error as err: if err.code == Gio.IOErrorEnum.NOT_FOUND: return gfile raise # The selected file exists, so we need to prompt for overwrite. parent_name = gfile.get_parent().get_parse_name() file_name = file_info.get_display_name() replace = modal_dialog( primary=_("Replace file “%s”?") % file_name, secondary=_( "A file with this name already exists in “%s”.\n" "If you replace the existing file, its contents " "will be lost.") % parent_name, buttons=[ (_("_Cancel"), Gtk.ResponseType.CANCEL), (_("_Replace"), Gtk.ResponseType.OK), ], messagetype=Gtk.MessageType.WARNING, ) if replace != Gtk.ResponseType.OK: return None return gfile
def on_button_delete_clicked(self, obj): files = self._get_selected_files() for name in files: try: gfile = Gio.File.new_for_path(name) gfile.trash(None) except GLib.GError as e: misc.error_dialog(_("Error removing %s") % name, str(e)) workdir = _commonprefix(files) self.refresh_partial(workdir)
def _make_copy_menu(self, chunk): copy_menu = Gtk.Menu() copy_up = Gtk.MenuItem.new_with_mnemonic(_("Copy _up")) copy_down = Gtk.MenuItem.new_with_mnemonic(_("Copy _down")) copy_menu.append(copy_up) copy_menu.append(copy_down) copy_menu.show_all() # FIXME: This is horrible widget = self.filediff.widget copy_menu.attach_to_widget(widget, None) def copy_chunk(widget, chunk, copy_up): self.filediff.copy_chunk(self.from_pane, self.to_pane, chunk, copy_up) copy_up.connect('activate', copy_chunk, chunk, True) copy_down.connect('activate', copy_chunk, chunk, False) return copy_menu
def append_filemerge(self, files, merge_output=None): if len(files) != 3: raise ValueError( _("Need three files to auto-merge, got: %r") % files) doc = filemerge.FileMerge(len(files)) self._append_page(doc, "text-x-generic") doc.set_files(files) if merge_output is not None: doc.set_merge_output_file(merge_output) return doc
def _find_text(self, start_offset=1, backwards=False, wrap=True): match_case = self.match_case.get_active() whole_word = self.whole_word.get_active() regex = self.regex.get_active() assert self.textview buf = self.textview.get_buffer() insert = buf.get_iter_at_mark(buf.get_insert()) tofind_utf8 = self.find_entry.get_text() tofind = tofind_utf8.decode("utf-8") start, end = buf.get_bounds() text = buf.get_text(start, end, False).decode("utf-8") if not regex: tofind = re.escape(tofind) if whole_word: tofind = r'\b' + tofind + r'\b' try: flags = re.M if match_case else re.M | re.I pattern = re.compile(tofind, flags) except re.error as e: misc.error_dialog(_("Regular expression error"), str(e)) else: self.wrap_box.set_visible(False) if not backwards: match = pattern.search(text, insert.get_offset() + start_offset) if match is None and wrap: self.wrap_box.set_visible(True) match = pattern.search(text, 0) else: match = None for m in pattern.finditer(text, 0, insert.get_offset()): match = m if match is None and wrap: self.wrap_box.set_visible(True) for m in pattern.finditer(text, insert.get_offset()): match = m if match: it = buf.get_iter_at_offset(match.start()) buf.place_cursor(it) it.forward_chars(match.end() - match.start()) buf.move_mark(buf.get_selection_bound(), it) self.textview.scroll_to_mark( buf.get_insert(), 0.25, True, 0.5, 0.5) return True else: buf.place_cursor(buf.get_iter_at_mark(buf.get_insert())) # FIXME: Even though docs suggest this should work, it does # not. It just sets the selection colour on the text, without # affecting the entry colour at all. color = Gdk.RGBA() color.parse("#ffdddd") self.find_entry.override_background_color( Gtk.StateType.NORMAL, color) self.wrap_box.set_visible(False)
class LabeledObjectMixin(GObject.GObject): __gsignals__ = { 'label-changed': (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_STRING, GObject.TYPE_STRING)), } label_text = _("untitled") tooltip_text = None def label_changed(self): self.emit("label-changed", self.label_text, self.tooltip_text)
def make_file_from_command_line(arg): f = command_line.create_file_for_arg(arg) if not f.query_exists(cancellable=None): # May be a relative path with ':', misinterpreted as a URI cwd = Gio.File.new_for_path(command_line.get_cwd()) relative = Gio.File.resolve_relative_path(cwd, arg) if relative.query_exists(cancellable=None): return relative # Return the original arg for a better error message if f.get_uri() is None: raise ValueError(_("invalid path or URI “%s”") % arg) # TODO: support for directories specified by URIs file_type = f.query_file_type(Gio.FileQueryInfoFlags.NONE, None) if not f.is_native() and file_type == Gio.FileType.DIRECTORY: raise ValueError( _("remote folder “{}” not supported").format(arg)) return f
def append_diff(self, paths, auto_compare=False, auto_merge=False, merge_output=None, meta=None): dirslist = [p for p in paths if os.path.isdir(p)] fileslist = [p for p in paths if os.path.isfile(p)] if dirslist and fileslist: raise ValueError(_("Cannot compare a mixture of files and directories")) elif dirslist: return self.append_dirdiff(paths, auto_compare) elif auto_merge: return self.append_filemerge(paths, merge_output=merge_output) else: return self.append_filediff(paths, merge_output=merge_output, meta=meta)
def __init__(self, iconname, text, onclose): Gtk.HBox.__init__(self, homogeneous=False, spacing=4) label = Gtk.Label(label=text) # FIXME: ideally, we would use custom ellipsization that ellipsized the # two paths separately, but that requires significant changes to label # generation in many different parts of the code label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) label.set_single_line_mode(True) label.set_alignment(0.0, 0.5) label.set_padding(0, 0) context = self.get_pango_context() font_desc = self.get_style_context().get_font(Gtk.StateFlags.NORMAL) metrics = context.get_metrics(font_desc, context.get_language()) char_width = metrics.get_approximate_char_width() / Pango.SCALE valid, w, h = Gtk.icon_size_lookup_for_settings( self.get_settings(), Gtk.IconSize.MENU) # FIXME: PIXELS replacement self.set_size_request( self.tab_width_in_chars * char_width + 2 * w, -1) button = Gtk.Button() button.set_relief(Gtk.ReliefStyle.NONE) button.set_focus_on_click(False) icon = Gio.ThemedIcon.new_with_default_fallbacks( 'window-close-symbolic') image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.MENU) image.set_tooltip_text(_("Close tab")) button.add(image) button.set_name("meld-tab-close-button") button.connect("clicked", onclose) context = button.get_style_context() provider = Gtk.CssProvider() provider.load_from_data(self.css) context.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) icon = Gtk.Image.new_from_icon_name(iconname, Gtk.IconSize.MENU) label_box = Gtk.EventBox() label_box.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) label_box.props.visible_window = False label_box.connect("button-press-event", self.on_label_clicked) label_box.add(label) self.pack_start(icon, False, True, 0) self.pack_start(label_box, True, True, 0) self.pack_start(button, False, True, 0) self.set_tooltip_text(text) self.show_all() self.__label = label self.__onclose = onclose
def update_filename_filters(self, *args): filter_items_model = Gio.Menu() for i, filt in enumerate(meldsettings.file_filters): name = FILE_FILTER_ACTION_FORMAT.format(i) filter_items_model.append( label=filt.label, detailed_action=f'view.{name}') section = Gio.MenuItem.new_section(_("Filename"), filter_items_model) section.set_attribute([("id", "s", "custom-filter-section")]) app = self.get_application() filter_model = app.get_menu_by_id("folder-status-filter-menu") replace_menu_section(filter_model, section)
def append_filemerge(self, gfiles, merge_output=None): if len(gfiles) != 3: raise ValueError( _("Need three files to auto-merge, got: %r") % [f.get_parse_name() for f in gfiles]) doc = FileMerge(len(gfiles)) self._append_page(doc, "text-x-generic") doc.set_files(gfiles) if merge_output is not None: doc.set_merge_output_file(merge_output) return doc
def _merge_files(self): yield _("[%s] Merging files") % self.label_text merger = merge.Merger() step = merger.initialize(self.buffer_filtered, self.buffer_texts) while next(step) is None: yield 1 for merged_text in merger.merge_3_files(): yield 1 self.linediffer.unresolved = merger.unresolved self.textbuffer[1].set_text(merged_text) self.recompute_label()
def append_new_comparison(self): doc = NewDiffTab(self) self._append_page(doc, "document-new") self.notebook.on_label_changed(doc, _("New comparison"), None) def diff_created_cb(doc, newdoc): doc.on_delete_event() idx = self.notebook.page_num(newdoc) self.notebook.set_current_page(idx) doc.connect("diff-created", diff_created_cb) return doc
def add_action_msg(self, icon, primary, secondary, action_label, callback): def on_response(msgarea, response_id, *args): self.clear() if response_id == Gtk.ResponseType.ACCEPT: callback() msgarea = self.new_from_text_and_icon(icon, primary, secondary) msgarea.add_button(action_label, Gtk.ResponseType.ACCEPT) msgarea.add_button(_("Hi_de"), Gtk.ResponseType.CLOSE) msgarea.connect("response", on_response) msgarea.show_all() return msgarea