From 00a29c95a82433aa255e75b088c279075cda9a70 Mon Sep 17 00:00:00 2001 From: Stefan Salewski Date: Thu, 30 Jul 2020 21:44:20 +0200 Subject: [PATCH] some fixes, mostly for GTK4 --- examples/gtk3/css_colored_listview.nim | 2 +- examples/gtk4/textview.nim | 82 ++++++++++++ gintro/gimpl.nim | 78 +++++++---- tests/gen.nim | 177 ++++++++++++++----------- 4 files changed, 231 insertions(+), 108 deletions(-) create mode 100644 examples/gtk4/textview.nim diff --git a/examples/gtk3/css_colored_listview.nim b/examples/gtk3/css_colored_listview.nim index 06c36fd..1f97a9b 100644 --- a/examples/gtk3/css_colored_listview.nim +++ b/examples/gtk3/css_colored_listview.nim @@ -38,7 +38,7 @@ proc listStore(o: gobject.Object): gtk.ListStore = assert(typeTest(o, "GtkListStore")) cast[gtk.ListStore](o) -proc updateRow(renderer: CellRendererText; path: cstring; newText: cstring; tree: TreeView) = +proc updateRow(renderer: CellRendererText; path: string; newText: string; tree: TreeView) = var iter: TreeIter var value: Value let gtype = typeFromName("gchararray") diff --git a/examples/gtk4/textview.nim b/examples/gtk4/textview.nim new file mode 100644 index 0000000..1e2be84 --- /dev/null +++ b/examples/gtk4/textview.nim @@ -0,0 +1,82 @@ +# nim c textview.nim +# ./textview textview.nim +# minimal GtkApplication example +import gintro/[gtk4, gobject, glib, gio] # , gtksource] # gtksource is not yet available for GTK4 + +from OS import paramCount, paramStr + +proc shutdown(app: Application) = + echo "shutdown" + +proc startup(app: Application) = + echo "startup" + +proc handleLocalOptions(app: Application; vd: VariantDict): int = + echo "handle-local-options" + +proc nameLost(app: Application): bool = + echo "name-lost" + +proc open(app: Application; files: seq[GFile]; hint: string) = + var + contents: string + etagOut: string + length: uint64 + buffer: TextBuffer + window: gtk4.Window + view: gtk4.TextView + echo "open" + for f in files: + echo f.uri + window = app.getActiveWindow + if window != nil: # instead of opening a new window reuse existing one + let h = ScrolledWindow(window.getChild) + view = TextView(h.getChild) + else: + window = newApplicationWindow(app) + window.title = "Text View" + window.defaultSize = (800, 600) + let scrolledWindow = newScrolledWindow() + view = newTextView() # gtksource.newView() + window.setChild(scrolledWindow) # add() for GTK3 + scrolledWindow.setChild(view) # add() for GTK3 + if files.len > 0: + if loadContents(files[0], cancellable = nil, contents, length, etagOut): + assert length.int == contents.len + echo "hint: ", hint + echo "etag: ", etagOut + buffer = view.getBuffer + buffer.setText(contents, contents.len) + + show(window) # showAll() for GTK3 + +proc commandLine(app: Application; cl: ApplicationCommandLine): int = + echo "command-line" + +proc activate(app: Application) = + echo "activate" + let window = newApplicationWindow(app) + window.title = "Empty Text View" + window.defaultSize = (800, 600) + let scrolledWindow = newScrolledWindow() + let view = newTextView() # gtksource.newView() + window.setChild(scrolledWindow) # add() for GTK3 + scrolledWindow.setChild(view) + show(window) # showAll() for GTK3 + +proc main = + let app = newApplication("org.gtk.example", {ApplicationFlag.handlesOpen})#, handlesCommandLine}) + app.connect("startup", startup) + app.connect("activate", activate) + app.connect("command-line", commandLine) + # app.connect("handle_local_options", handleLocalOptions) + app.connect("open", open) + app.connect("name-lost", nameLost) + app.connect("shutdown", shutdown) + let argLen = paramCount() + 1 + var argStr = newSeq[string](argLen) + for i in 0 ..< argLen: + argStr[i] = paramStr(i) + discard run(app, argLen, argStr) # we have to pass an argString to support open signal handling files + +main() diff --git a/gintro/gimpl.nim b/gintro/gimpl.nim index 63cc981..482df41 100644 --- a/gintro/gimpl.nim +++ b/gintro/gimpl.nim @@ -1,3 +1,4 @@ +from sequtils import keepItIf const RecSep = "!" # Record separator for entries in gisup.nim, also defined in gen.nim # caution: $$ due to string interpolation @@ -97,6 +98,7 @@ proc $1$2 {.cdecl.} = r1s.add(")\n") else: # signals with multiple arguments and maybe using interface providers (sn, wid, num, ahl, all) = sci.split(RecSep) + echo "ttt ", ahl if ahl.contains("|"): # we have to handle interface providers var hargs = ahl.split(";") # handler arguments for i in 0 .. hargs.high: # find the position of the providers, should be second or third argument @@ -128,10 +130,24 @@ proc $1$2 {.cdecl.} = if all.find(";") > 0: # more than one argument var largs = all.split("; ") largs.delete(0) - largs[^1] = largs[^1].split(")")[0] + largs[^1] = largs[^1].split(")")[0] var names, types: array[10, string] - for i in 0 .. largs.high: - if largs[i].endsWith("00"): + #for i in 0 .. largs.high: + var i = 0 + var largslen = largs.len + while i < largslen: + if largs[i].endsWith("00Array"): + (names[i], types[i]) = largs[i].split(": ") + let al = largs[i + 1].split(": ")[0] + var h = types[i] + h[0] = h[0].toLowerAscii + h.add("2seq(" & names[i] & ", " & al & ")") + names[i] = h + types[i].setLen(0) + #largs.del(i + 1) # bug, should be delete! + largs.delete(i + 1) + dec(largslen) + elif largs[i].endsWith("00"): (names[i], types[i]) = largs[i].split(": ptr ") types[i].setLen(types[i].len - 2) else: @@ -142,6 +158,7 @@ proc $1$2 {.cdecl.} = if h.len > 0: a1 = h & "(" & a1 & ")" names[i] = a1 + inc(i) if ipos > 0: types[ipos - 1] = ipro @@ -165,11 +182,17 @@ proc $1$2 {.cdecl.} = resu.add(")") if resl == "gboolean": resu.add(".ord.gboolean") + + if resl == "int32": + resu.add(".int32") + r1s.add(resu & "\n") all = all.replace(")", "; user_data: pointer)") r1s = r1s % [$procNameCdecl, all, $p, wts, ats] - # echo r1s + #echo r1s + result = parseStmt(r1s) + if not ignoreArg.boolVal: ahl = ahl.replace(")", "; arg: " & ats & ")") if ahl.find(";") > 0: @@ -177,29 +200,27 @@ proc $1$2 {.cdecl.} = else: ahl = "(self: " & wts & ")" & ahl.split(")", 1)[1] - let r2sunused = - if ignoreArg.boolVal: - """ -proc $1(self: $2; p: proc $3): culong {.discardable.} = - sc$4(self, $5, nil) -$1($6, $7) -""" % [$procName, wts, ahl, signalName, $procNameCdecl, $(widget.toStrLit), $p] - else: - """ -proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} = - when a is ref: - GC_ref(a) - sc$5(self, $6, cast[pointer](a)) - else: - var ar: ref $4 - new(ar) - #deepCopy(ar[], a) - ar[] = a - GC_ref(ar) - # sc$5(self, $6, cast[pointer](ar[])) - sc$5(self, $6, cast[pointer](ar)) -$1($7, $8, $9) -""" % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit)] + if ahl.find("00Array") >= 0: + var hhh = ahl.split("; ") + let l = hhh.len + var i = 0 + while i < l: + # files: seq[gio.File] + if hhh[i] .find("00Array") >= 0: + var a, b: string + (a, b) = hhh[i].split(": ") + b = b.replace("00Array", "]") + #if b == "File]": + # b = "gio.File]" + b = "seq[" & b + hhh[i] = a & ": " & b + hhh[i + 1].setLen(0) + inc(i) + hhh.keepItIf(it.len > 0) + #echo ">>>>>>>>>> ", hhh.join("; ") + ahl = hhh.join("; ") + + ahl = ahl.replace(": cstring", ": string") # maybe we write this better this way: let r2s = @@ -228,7 +249,8 @@ proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} = sc$5(self, $6, cast[pointer](ar), $10) $1($7, $8, $9) """ % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit), sfstr] - # echo r2s + #echo r2s + result.add(parseStmt(r2s)) template connect*(widget: gobject.Object; signal: string; p: typed; arg: typed): untyped = diff --git a/tests/gen.nim b/tests/gen.nim index 69b9735..adba104 100644 --- a/tests/gen.nim +++ b/tests/gen.nim @@ -1,6 +1,6 @@ # High level gobject-introspection based GTK3/GTK4 bindings for the Nim programming language # nimpretty --maxLineLen:130 gen.nim -# v 0.7.9 2020-JUL-13 +# v 0.8.0 2020-JUL-30 # (c) S. Salewski 2018 # https://wiki.gnome.org/Projects/GObjectIntrospection @@ -194,52 +194,52 @@ var suppressRaise = false # fixedDestroyNames.add("gdk_cairo_region_create_from_surface", "cairo.destroy") # fixedDestroyNames.add("gdk_cairo_surface_create_from_pixbuf", "cairo.destroy") -fixedProcNames.add("gtk_button_new_with_label", "newButton") -fixedProcNames.add("pango_font_description_from_string", "newFontDescription") -fixedProcNames.add("pango_language_from_string", "languageFromString") -fixedProcNames.add("atk_role_register", "registerAtkRole") -fixedProcNames.add("atk_state_type_register", "registerStateType") -fixedProcNames.add("atk_relation_type_register", "registerRelationType") -fixedProcNames.add("atk_text_attribute_register", "registerTextAttribute") -fixedProcNames.add("atk_role_for_name", "roleForName") -fixedProcNames.add("atk_state_type_for_name", "stateTypeForName") -fixedProcNames.add("atk_relation_type_for_name", "relationTypeForName") -fixedProcNames.add("atk_text_attribute_for_name", "textAttributeForName") -fixedProcNames.add("gtk_settings_get_for_screen", "getSettingsForScreen") -fixedProcNames.add("gtk_icon_theme_get_for_screen", "getIconThemeForScreen") -fixedProcNames.add("gtk_requisition_new", "newRequisition") -fixedProcNames.add("gtk_text_attributes_new", "newTextAttributes") -fixedProcNames.add("pango_script_for_unichar", "scriptForUnichar") -fixedProcNames.add("pango_bidi_type_for_unichar", "bidiTypeForUnichar") -fixedProcNames.add("gsk_shadow_node_get_child", "shadowNodeGetChild") -fixedProcNames.add("gsk_transform_node_get_child", "transformNodeGetChild") -fixedProcNames.add("gtk_settings_get_for_display", "getSettingsForDisplay") -fixedProcNames.add("gtk_icon_theme_get_for_display", "getIconThemeForDisplay") -fixedProcNames.add("gst_element_factory_find", "findElementFactory") +fixedProcNames["gtk_button_new_with_label"] = "newButton" +fixedProcNames["pango_font_description_from_string"] = "newFontDescription" +fixedProcNames["pango_language_from_string"] = "languageFromString" +fixedProcNames["atk_role_register"] = "registerAtkRole" +fixedProcNames["atk_state_type_register"] = "registerStateType" +fixedProcNames["atk_relation_type_register"] = "registerRelationType" +fixedProcNames["atk_text_attribute_register"] = "registerTextAttribute" +fixedProcNames["atk_role_for_name"] = "roleForName" +fixedProcNames["atk_state_type_for_name"] = "stateTypeForName" +fixedProcNames["atk_relation_type_for_name"] = "relationTypeForName" +fixedProcNames["atk_text_attribute_for_name"] = "textAttributeForName" +fixedProcNames["gtk_settings_get_for_screen"] = "getSettingsForScreen" +fixedProcNames["gtk_icon_theme_get_for_screen"] = "getIconThemeForScreen" +fixedProcNames["gtk_requisition_new"] = "newRequisition" +fixedProcNames["gtk_text_attributes_new"] = "newTextAttributes" +fixedProcNames["pango_script_for_unichar"] = "scriptForUnichar" +fixedProcNames["pango_bidi_type_for_unichar"] = "bidiTypeForUnichar" +fixedProcNames["gsk_shadow_node_get_child"] = "shadowNodeGetChild" +fixedProcNames["gsk_transform_node_get_child"] = "transformNodeGetChild" +fixedProcNames["gtk_settings_get_for_display"] = "getSettingsForDisplay" +fixedProcNames["gtk_icon_theme_get_for_display"] = "getIconThemeForDisplay" +fixedProcNames["gst_element_factory_find"] = "findElementFactory" -fixedProcNames.add("gtk_shortcut_action_parse_string", "shortcutActionParseString") -fixedProcNames.add("gtk_shortcut_trigger_parse_string", "shortcutTriggerParseString") -fixedProcNames.add("g_main_context_default", "defaultMainContext") +fixedProcNames["gtk_shortcut_action_parse_string"] = "shortcutActionParseString" +fixedProcNames["gtk_shortcut_trigger_parse_string"] = "shortcutTriggerParseString" +fixedProcNames["g_main_context_default"] = "defaultMainContext" -defaultParameters.add("gtk_window_new", "`type` WindowType WindowType.toplevel") -defaultParameters.add("gtk_application_new", "flags gio.ApplicationFlags {}") -defaultParameters.add("gtk_builder_new_from_string", "length int64 -1") -defaultParameters.add("gtk_box_new", "spacing int32 0") +defaultParameters["gtk_window_new"] = "`type` WindowType WindowType.toplevel" +defaultParameters["gtk_application_new"] = "flags gio.ApplicationFlags {}" +defaultParameters["gtk_builder_new_from_string"] = "length int64 -1" +defaultParameters["gtk_box_new"] = "spacing int32 0" -for i in keywords: mangledNames.add(i, '`' & i & '`') +for i in keywords: mangledNames[i] = '`' & i & '`' -mangledNames.add("CSET_A_2_Z", "CSET_A_2_Z_U") -mangledNames.add("CSET_a_2_z", "CSET_a_2_z_L") -mangledNames.add("2big", "tooBig") -mangledNames.add("2buttonPress", "twoButtonPress") -mangledNames.add("3buttonPress", "threeButtonPress") -mangledNames.add("result", "resu") -mangledNames.add("3d", "d3") -mangledNames.add("2d", "d2") -mangledNames.add("2dAffine", "d2Affine") -mangledNames.add("2dTranslate", "d2Translate") -mangledNames.add("true", "true1") -mangledNames.add("false", "false0") +mangledNames["CSET_A_2_Z"] = "CSET_A_2_Z_U" +mangledNames["CSET_a_2_z"] = "CSET_a_2_z_L" +mangledNames["2big"] = "tooBig" +mangledNames["2buttonPress"] = "twoButtonPress" +mangledNames["3buttonPress"] = "threeButtonPress" +mangledNames["result"] = "resu" +mangledNames["3d"] = "d3" +mangledNames["2d"] = "d2" +mangledNames["2dAffine"] = "d2Affine" +mangledNames["2dTranslate"] = "d2Translate" +mangledNames["true"] = "true1" +mangledNames["false"] = "false0" const someEvent: HashSet[string] = """" EventAny EventKey EventButton EventTouch EventScroll EventMotion EventExpose EventVisibility @@ -257,6 +257,8 @@ proc gBaseInfoGetName(info: GIBaseInfo): string = let h = $(gir.gBaseInfoGetNamespace(info)) if h == "Gio" and result == "Application": result = "GApplication" + if h == "Gio" and result == "File": + result = "GFile" proc mangleName(s: cstring): string = result = mysnakeToCamel(s) @@ -265,23 +267,23 @@ proc mangleName(s: cstring): string = # assert(false) result = "xxx" # XXX -mangledTypes.add("gint8", "int8") -mangledTypes.add("gint16", "int16") -mangledTypes.add("gint32", "int32") -mangledTypes.add("gint64", "int64") -mangledTypes.add("guint8", "uint8") -mangledTypes.add("guint16", "uint16") -mangledTypes.add("guint32", "uint32") -mangledTypes.add("guint64", "uint64") -mangledTypes.add("gfloat", "cfloat") -mangledTypes.add("gdouble", "cdouble") -mangledTypes.add("ptr void", "pointer") -mangledTypes.add("ptr filename", "cstring") -mangledTypes.add("ptr error", "ptr glib.Error") -mangledTypes.add("ptr ghash", "ptr HashTable00") -mangledTypes.add("ptr glist", "ptr pointer") -mangledTypes.add("ptr gslist", "ptr pointer") -mangledTypes.add("ptr utf8", "cstring") +mangledTypes["gint8"] = "int8" +mangledTypes["gint16"] = "int16" +mangledTypes["gint32"] = "int32" +mangledTypes["gint64"] = "int64" +mangledTypes["guint8"] = "uint8" +mangledTypes["guint16"] = "uint16" +mangledTypes["guint32"] = "uint32" +mangledTypes["guint64"] = "uint64" +mangledTypes["gfloat"] = "cfloat" +mangledTypes["gdouble"] = "cdouble" +mangledTypes["ptr void"] = "pointer" +mangledTypes["ptr filename"] = "cstring" +mangledTypes["ptr error"] = "ptr glib.Error" +mangledTypes["ptr ghash"] = "ptr HashTable00" +mangledTypes["ptr glist"] = "ptr pointer" +mangledTypes["ptr gslist"] = "ptr pointer" +mangledTypes["ptr utf8"] = "cstring" #utf8StringArrays.incl("g_strv_length") # bugs in GIR files # TODO needs investigation #utf8StringArrays.incl("g_strfreev") @@ -499,17 +501,17 @@ proc needProxyProc(info: GICallableInfo): bool = # type replacements var ct2nt: Table[string, string] -ct2nt.add("uint32", "int") -ct2nt.add("int32", "int") -ct2nt.add("cstring", "string") -ct2nt.add("gboolean", "bool") -ct2nt.add("cstringArray", "seq[string]") -ct2nt.add("int32Array", "seq[int32]") -ct2nt.add("uint32Array", "seq[uint32]") -ct2nt.add("uint8Array", "seq[uint8]") -ct2nt.add("TargetEntry00Array", "seq[TargetEntry]") -ct2nt.add("KeymapKeyArray", "seq[KeymapKey]") -ct2nt.add("PageRangeArray", "seq[PageRange]") +ct2nt["uint32"] = "int" +ct2nt["int32"] = "int" +ct2nt["cstring"] = "string" +ct2nt["gboolean"] = "bool" +ct2nt["cstringArray"] = "seq[string]" +ct2nt["int32Array"] = "seq[int32]" +ct2nt["uint32Array"] = "seq[uint32]" +ct2nt["uint8Array"] = "seq[uint8]" +ct2nt["TargetEntry00Array"] = "seq[TargetEntry]" +ct2nt["KeymapKeyArray"] = "seq[KeymapKey]" +ct2nt["PageRangeArray"] = "seq[PageRange]" # converter procs proc ct3nt(s: string): string = @@ -636,11 +638,11 @@ proc genPars(info: GICallableInfo; genProxy = false; binfo: GIBaseInfo = nil): ( else: arglist.add(name) if not genProxy and ct2nt.contains(str) and (str != "cstring" or gArgInfoGetDirection(arg) in {GIDirection.OUT, INOUT}): - replist.add($name, $str) + replist[$name] = $str if genProxy and ct2nt.contains(str): if gArgInfoGetDirection(arg) in {GIDirection.OUT, INOUT} and (not gArgInfoIsCallerAllocates(arg) or (str in ["int32", "uint32", "cstringArray", "cstring"])): - replist.add($name, $str) + replist[$name] = $str if gArgInfoGetDirection(arg) in {GIDirection.OUT, INOUT} or str != "cstring": # new in 0.4.11 -- allow passing cstring if not a var str = ct2nt[str] if gArgInfoGetDirection(arg) in {GIDirection.OUT, INOUT} and not (str in UnnamedArrays and gArgInfoIsCallerAllocates(arg)): @@ -666,13 +668,20 @@ proc genPars(info: GICallableInfo; genProxy = false; binfo: GIBaseInfo = nil): ( var h = str h.removePrefix("var ") str.add(" = cast[ptr " & h & "](nil)[]") + + #if str == "var seq[string]": no var varargs support! if str == "seq[string]": if j == m: str = "varargs[string, `$`]" else: str = "openArray[string]" if str == "seq[uint8]": # allow to pass a string for a seq[uint8] argument too + # str = "seq[uint8] | openArray[char]" # maybe also seq[int8] and seq[char] ? # https://github.com/nim-lang/Nim/issues/15128 str = "seq[uint8] | string" + if str == "var seq[uint8]": # allow to pass a string for a seq[uint8] argument too + str = "var (seq[uint8] | string)" + # str = "var (seq[uint8] | openArray[char])" # https://github.com/nim-lang/Nim/issues/15128 + resus.add((name, str)) if j < m: arglist.add(", ") @@ -791,8 +800,6 @@ proc writeMethod(info: GIBaseInfo; minfo: GIFunctionInfo) = # no, will fail i.e. for gdk_cursor_new_from_name() methodBuffer.writeLine(" result = cast[type(result)](qdata)") methodBuffer.writeLine(" assert(result.impl == gobj)") - #methodBuffer.writeLine(" echo \"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\"") - #methodBuffer.writeLine(" echo \"YXC $1 \", result.refCount" % [sym]) methodBuffer.writeLine(" else:") #assert((gFunctionInfoGetFlags(mInfo).int and GIFunctionInfoFlags.IS_CONSTRUCTOR.int) != 0 or # gCallableInfoGetCallerOwns(minfo) == GITransfer.EVERYTHING) @@ -811,7 +818,6 @@ proc writeMethod(info: GIBaseInfo; minfo: GIFunctionInfo) = methodBuffer.writeLine(" g_object_unref(result.impl)") methodBuffer.writeLine(" assert(g_object_get_qdata(result.impl, Quark) == nil)") methodBuffer.writeLine(" g_object_set_qdata(result.impl, Quark, addr(result[]))") - #methodBuffer.writeLine(" echo \"YXC $1 \", result.refCount" % [sym]) # prefer gboxed_free() but fall back for GVariant template findFreeProc(info: GIBaseInfo; noWarning: bool): untyped = @@ -893,7 +899,7 @@ proc writeMethod(info: GIBaseInfo; minfo: GIFunctionInfo) = if sym == "g_file_get_contents": return # fix later if sym == "g_spawn_command_line_sync": return # fix later if sym == "g_spawn_sync": return # fix later - if sym == "g_file_load_contents": return # fix later + ###if sym == "g_file_load_contents": return # fix later #if sym == "g_file_load_contents_finish": return # fix later if sym == "g_file_load_partial_contents_finish": return # fix later if sym == "g_converter_convert": return # fix later @@ -1582,9 +1588,11 @@ template writeSignal() = var plist, arglist: string var replist: TableRef[string, string] (plist, arglist, replist) = genPars(signalInfo, true, info) + #echo plist memo.add(plist) memo.add(RecSep) (plist, arglist, replist) = genPars(signalInfo, false, info) + #echo plist memo.add(plist) memo = memo.replace("\n ", " ") memo = memo.replace("\"", "\\\"") @@ -2160,6 +2168,17 @@ const GIO_EPI = """ proc run*(self: GApplication): int = int(g_application_run(cast[ptr GApplication00](self.impl), 0, nil)) +proc gFile00Array2seq*(p: GFile00Array; l: int32): seq[gio.GFile] = + let a = cast[ptr UncheckedArray[ptr GFile00]](p) + result = newSeq[gio.GFile]() + var i: int + while i < l: + let f = new gio.GFile + f.impl = a[i] + f.ignoreFinalizer = true + result.add(f) + inc(i) + """ const GObject_EPI = """ @@ -2564,7 +2583,7 @@ proc uint8ArrayZT2seq*(p: pointer): seq[uint8] = if gdkKeys.contains(h.toLowerAscii): gdkKeys[h.toLowerAscii].add(h) else: - gdkKeys.add(h.toLowerAscii, @[h]) + gdkKeys[h.toLowerAscii] = @[h] for el in mvalues(gdkKeys): sort(el, system.cmp) var i: GIBaseInfo @@ -2903,4 +2922,4 @@ proc launch() = supmod4.close launch() -# 2906 lines +# 2925 lines